fix: batch post-483 workspace follow-ups (#508)
Co-authored-by: Aurora release bot <release@outsourc-e.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
# Or pull pre-built:
|
# Or pull pre-built:
|
||||||
# docker pull ghcr.io/outsourc-e/hermes-workspace:latest
|
# docker pull ghcr.io/outsourc-e/hermes-workspace:latest
|
||||||
#
|
#
|
||||||
FROM tianon/gosu:1.19-bookworm AS gosu_source
|
FROM tianon/gosu:1.17-bookworm AS gosu_source
|
||||||
# ─── build stage ─────────────────────────────────────────────────────────
|
# ─── build stage ─────────────────────────────────────────────────────────
|
||||||
FROM node:22-slim AS build
|
FROM node:22-slim AS build
|
||||||
RUN corepack enable && apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
RUN corepack enable && apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { buildWorkspaceScopedTextMessage } from './workspace-message-scope'
|
import {
|
||||||
|
buildWorkspaceScopedTextMessage,
|
||||||
|
stripWorkspaceDirective,
|
||||||
|
} from './workspace-message-scope'
|
||||||
|
|
||||||
describe('buildWorkspaceScopedTextMessage', () => {
|
describe('buildWorkspaceScopedTextMessage', () => {
|
||||||
it('prepends an explicit active workspace directive to plain text chat messages', () => {
|
it('prepends an explicit active workspace directive to plain text chat messages', () => {
|
||||||
@@ -39,4 +42,12 @@ describe('buildWorkspaceScopedTextMessage', () => {
|
|||||||
}),
|
}),
|
||||||
).toBe('hello')
|
).toBe('hello')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('strips the workspace directive back out for user-visible rendering', () => {
|
||||||
|
expect(
|
||||||
|
stripWorkspaceDirective(
|
||||||
|
'<workspace_context active="true" name="Home" path="/Users/aurora/workspace" />\n\nRun the tests',
|
||||||
|
),
|
||||||
|
).toBe('Run the tests')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ export type WorkspaceScope = {
|
|||||||
isValid?: boolean
|
isValid?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WORKSPACE_DIRECTIVE_RE =
|
||||||
|
/^\s*<workspace_context\s+active="true"\s+name="[^"]*"\s+path="[^"]*"\s*\/?>\s*/i
|
||||||
|
|
||||||
function escapeAttribute(value: string): string {
|
function escapeAttribute(value: string): string {
|
||||||
return value
|
return value
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -28,3 +31,8 @@ export function buildWorkspaceScopedTextMessage(
|
|||||||
if (!directive) return message
|
if (!directive) return message
|
||||||
return `${directive}\n\n${message}`
|
return `${directive}\n\n${message}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stripWorkspaceDirective(message: string): string {
|
||||||
|
if (!message.includes('<workspace_context active="true"')) return message
|
||||||
|
return message.replace(WORKSPACE_DIRECTIVE_RE, '').trimStart()
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
isRecentSession,
|
isRecentSession,
|
||||||
resetPendingSend,
|
resetPendingSend,
|
||||||
setPendingGeneration,
|
setPendingGeneration,
|
||||||
|
clearPendingSendForSession,
|
||||||
} from './pending-send'
|
} from './pending-send'
|
||||||
import { useChatMeasurements } from './hooks/use-chat-measurements'
|
import { useChatMeasurements } from './hooks/use-chat-measurements'
|
||||||
import { useChatHistory } from './hooks/use-chat-history'
|
import { useChatHistory } from './hooks/use-chat-history'
|
||||||
@@ -100,7 +101,7 @@ import { MobileSessionsPanel } from '@/components/mobile-sessions-panel'
|
|||||||
import { ContextAlertModal } from '@/components/usage-meter/context-alert-modal'
|
import { ContextAlertModal } from '@/components/usage-meter/context-alert-modal'
|
||||||
import { ErrorToastContainer, showErrorToast } from '@/components/error-toast'
|
import { ErrorToastContainer, showErrorToast } from '@/components/error-toast'
|
||||||
// ContextMeter removed — ContextBar (PR #32) replaces it
|
// ContextMeter removed — ContextBar (PR #32) replaces it
|
||||||
import { useChatStore } from '@/stores/chat-store'
|
import { useChatStore, persistRecoveryMessage } from '@/stores/chat-store'
|
||||||
import { useResearchCard } from '@/hooks/use-research-card'
|
import { useResearchCard } from '@/hooks/use-research-card'
|
||||||
// MOBILE_TAB_BAR_OFFSET removed — tab bar always hidden in chat
|
// MOBILE_TAB_BAR_OFFSET removed — tab bar always hidden in chat
|
||||||
import { useTapDebug } from '@/hooks/use-tap-debug'
|
import { useTapDebug } from '@/hooks/use-tap-debug'
|
||||||
@@ -1128,7 +1129,7 @@ export function ChatScreen({
|
|||||||
},
|
},
|
||||||
[queryClient],
|
[queryClient],
|
||||||
),
|
),
|
||||||
onComplete: useCallback(() => {
|
onComplete: useCallback((message: ChatMessage) => {
|
||||||
const activeSend = activeSendRef.current
|
const activeSend = activeSendRef.current
|
||||||
if (activeSend?.clientId) {
|
if (activeSend?.clientId) {
|
||||||
updateHistoryMessageByClientIdEverywhere(
|
updateHistoryMessageByClientIdEverywhere(
|
||||||
@@ -1140,6 +1141,13 @@ export function ChatScreen({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (activeSend?.sessionKey) {
|
||||||
|
persistRecoveryMessage(activeSend.sessionKey, message)
|
||||||
|
clearPendingSendForSession(
|
||||||
|
activeSend.sessionKey,
|
||||||
|
activeSend.friendlyId,
|
||||||
|
)
|
||||||
|
}
|
||||||
activeSendRef.current = null
|
activeSendRef.current = null
|
||||||
refreshHistoryRef.current()
|
refreshHistoryRef.current()
|
||||||
setSending(false)
|
setSending(false)
|
||||||
|
|||||||
41
src/screens/chat/utils.test.ts
Normal file
41
src/screens/chat/utils.test.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { normalizeSessions, textFromMessage } from './utils'
|
||||||
|
import type { ChatMessage, SessionSummary } from './types'
|
||||||
|
|
||||||
|
describe('chat utils workspace directive cleanup', () => {
|
||||||
|
it('hides workspace_context directives from user-visible message text', () => {
|
||||||
|
const message: ChatMessage = {
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '<workspace_context active="true" name="Home" path="/Users/aurora/workspace" />\n\nRun the tests',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(textFromMessage(message)).toBe('Run the tests')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('strips workspace_context directives from session previews and derived titles', () => {
|
||||||
|
const sessions = normalizeSessions([
|
||||||
|
{
|
||||||
|
key: 'session-1',
|
||||||
|
friendlyId: 'session-1',
|
||||||
|
preview:
|
||||||
|
'<workspace_context active="true" name="Home" path="/Users/aurora/workspace" />\n\nReview the open PRs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'session-2',
|
||||||
|
friendlyId: 'session-2',
|
||||||
|
derivedTitle:
|
||||||
|
'<workspace_context active="true" name="Home" path="/Users/aurora/workspace" />\n\nFix Docker publish',
|
||||||
|
},
|
||||||
|
] satisfies Array<SessionSummary>)
|
||||||
|
|
||||||
|
expect(sessions[0]?.preview).toBe('Review the open PRs')
|
||||||
|
expect(sessions[0]?.derivedTitle).toBe('Review the open PRs')
|
||||||
|
expect(sessions[1]?.derivedTitle).toBe('Fix Docker publish')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
SessionTitleStatus,
|
SessionTitleStatus,
|
||||||
ToolCallContent,
|
ToolCallContent,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
import { stripWorkspaceDirective } from '../../lib/workspace-message-scope'
|
||||||
|
|
||||||
export function deriveFriendlyIdFromKey(key: string | undefined): string {
|
export function deriveFriendlyIdFromKey(key: string | undefined): string {
|
||||||
if (!key) return 'main'
|
if (!key) return 'main'
|
||||||
@@ -52,7 +53,7 @@ function stripChannelPrefix(text: string): string {
|
|||||||
* and [Telegram/Signal/etc ...] headers, leaving just the user's text.
|
* and [Telegram/Signal/etc ...] headers, leaving just the user's text.
|
||||||
*/
|
*/
|
||||||
function cleanUserText(raw: string): string {
|
function cleanUserText(raw: string): string {
|
||||||
let text = raw
|
let text = stripWorkspaceDirective(raw)
|
||||||
|
|
||||||
// Remove "Conversation info (untrusted metadata):" headers + JSON block
|
// Remove "Conversation info (untrusted metadata):" headers + JSON block
|
||||||
// Format: "Conversation info (untrusted metadata):\n```json\n{...}\n```\n\n"
|
// Format: "Conversation info (untrusted metadata):\n```json\n{...}\n```\n\n"
|
||||||
@@ -226,15 +227,15 @@ export function normalizeSessions(
|
|||||||
: undefined
|
: undefined
|
||||||
const explicitTitle =
|
const explicitTitle =
|
||||||
typeof session.title === 'string' && session.title.trim().length > 0
|
typeof session.title === 'string' && session.title.trim().length > 0
|
||||||
? session.title.trim()
|
? cleanUserText(session.title.trim()) || session.title.trim()
|
||||||
: undefined
|
: undefined
|
||||||
const derivedTitle =
|
const derivedTitle =
|
||||||
typeof session.derivedTitle === 'string' &&
|
typeof session.derivedTitle === 'string' &&
|
||||||
session.derivedTitle.trim().length > 0
|
session.derivedTitle.trim().length > 0
|
||||||
? session.derivedTitle.trim()
|
? cleanUserText(session.derivedTitle.trim()) || session.derivedTitle.trim()
|
||||||
: typeof session.preview === 'string' &&
|
: typeof session.preview === 'string' &&
|
||||||
session.preview.trim().length > 0
|
session.preview.trim().length > 0
|
||||||
? session.preview.trim()
|
? cleanUserText(session.preview.trim()) || session.preview.trim()
|
||||||
: undefined
|
: undefined
|
||||||
const titleStatus = deriveTitleStatus(
|
const titleStatus = deriveTitleStatus(
|
||||||
label,
|
label,
|
||||||
@@ -261,7 +262,10 @@ export function normalizeSessions(
|
|||||||
titleStatus,
|
titleStatus,
|
||||||
titleSource,
|
titleSource,
|
||||||
titleError: session.titleError ?? null,
|
titleError: session.titleError ?? null,
|
||||||
preview: session.preview ?? null,
|
preview:
|
||||||
|
typeof session.preview === 'string'
|
||||||
|
? cleanUserText(session.preview) || session.preview.trim() || null
|
||||||
|
: session.preview ?? null,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -478,7 +478,10 @@ const config = defineConfig(({ mode, command }) => {
|
|||||||
// 2. $PORT env var (for containers, reverse proxies, WhatsApp bridge collisions, etc. — see #96)
|
// 2. $PORT env var (for containers, reverse proxies, WhatsApp bridge collisions, etc. — see #96)
|
||||||
// 3. default 3000 (matches README/docs/docker-compose expectations)
|
// 3. default 3000 (matches README/docs/docker-compose expectations)
|
||||||
port: process.env.PORT ? Number(process.env.PORT) : 3000,
|
port: process.env.PORT ? Number(process.env.PORT) : 3000,
|
||||||
strictPort: false, // allow fallback if port is taken, but log clearly
|
// Managed Workspace launchers expect a stable port. Fail loudly instead
|
||||||
|
// of silently hopping to 3001+ so launchctl/service health matches the
|
||||||
|
// actual listening socket.
|
||||||
|
strictPort: true,
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
watch: {
|
watch: {
|
||||||
ignored: [
|
ignored: [
|
||||||
|
|||||||
Reference in New Issue
Block a user