feat(terminal): refactor runtime and add mobile workspace (#2280)
Replace the legacy terminal flow with a shared authenticated WebSocket runtime used across web, desktop, relay, and mobile surfaces. - introduce the v3 terminal protocol with scoped attachments, snapshots, ordered output, bounded replay history, reconnects, and explicit lifecycle - harden PTY creation, restart, resize, close, force-kill, idle cleanup, shell selection, login mode, environment sanitization, and appearance sync - add runtime-aware terminal APIs with relay authentication and Electron parity - add a fullscreen mobile terminal workspace with touch scrolling, long-press selection, safe-area controls, quick keys, and Ctrl/Alt input - add terminal selection attachments, preview detection, project actions, shell settings, and localized UI - harden Ghostty rendering, resize recovery, Unicode handling, block characters, line height, and stale-row behavior - remove the obsolete terminal SSE path and update reverse-proxy guidance - expand terminal runtime, transport, input, selection, and store coverage - avoid duplicate web builds when preparing mobile assets in root CI builds
This commit is contained in:
committed by
GitHub
parent
f5b4a267c0
commit
d4a8c4d2e1
@@ -14,6 +14,7 @@ import {
|
||||
parseSkillHref,
|
||||
} from '@/lib/messages/inlineMessageLinks';
|
||||
import { prepareUserMarkdownContent, SKILL_TOKEN_PATTERN } from './userTextPartContent';
|
||||
import { extractTerminalContexts } from '@/lib/messages/terminalContext';
|
||||
|
||||
type PartWithText = Part & { text?: string; content?: string; value?: string };
|
||||
|
||||
@@ -31,7 +32,9 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
|
||||
const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMention }) => {
|
||||
const partWithText = part as PartWithText;
|
||||
const rawText = partWithText.text;
|
||||
const textContent = typeof rawText === 'string' ? rawText : partWithText.content || partWithText.value || '';
|
||||
const serializedText = typeof rawText === 'string' ? rawText : partWithText.content || partWithText.value || '';
|
||||
const terminalContextState = React.useMemo(() => extractTerminalContexts(serializedText), [serializedText]);
|
||||
const textContent = terminalContextState.visibleText;
|
||||
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
const [isTruncated, setIsTruncated] = React.useState(false);
|
||||
@@ -190,7 +193,7 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
|
||||
});
|
||||
}, [agentMention, openSkill, skillByName, textContent]);
|
||||
|
||||
if (!textContent || textContent.trim().length === 0) {
|
||||
if ((!textContent || textContent.trim().length === 0) && terminalContextState.contexts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -243,6 +246,18 @@ const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMenti
|
||||
plainTextContent
|
||||
)}
|
||||
</div>
|
||||
{terminalContextState.contexts.length > 0 ? (
|
||||
<div className="mt-2 space-y-1.5">
|
||||
{terminalContextState.contexts.map((context, index) => (
|
||||
<details key={`${context.terminalLabel}-${context.startLine}-${index}`} className="rounded-md border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-2 py-1.5 text-xs">
|
||||
<summary className="cursor-pointer text-[var(--surface-mutedForeground)]">
|
||||
{t('chat.message.terminalContext', { terminal: context.terminalLabel, start: context.startLine, end: context.endLine })}
|
||||
</summary>
|
||||
<pre className="mt-2 max-h-48 overflow-auto whitespace-pre-wrap font-mono text-[var(--surface-foreground)]">{context.text}</pre>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user