fix: batch post-483 workspace follow-ups (#508)

Co-authored-by: Aurora release bot <release@outsourc-e.com>
This commit is contained in:
Eric
2026-05-23 20:10:45 -04:00
committed by GitHub
parent fbd7877466
commit b69aa347ad
7 changed files with 85 additions and 10 deletions

View File

@@ -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/*

View File

@@ -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')
})
}) })

View File

@@ -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, '&amp;') .replace(/&/g, '&amp;')
@@ -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()
}

View File

@@ -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)

View 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')
})
})

View File

@@ -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,
} }
}) })
} }

View File

@@ -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: [