fix(web-read-card): align highlight grammars with read hints, fix fixture schema, restore running sweep
Register the full grammar set the read tool's langFromPath emits (python,
go, rust, yaml, markdown, html, and the rest) so a read card highlights the
same extensions the backend recognizes instead of returning undefined for
them. Rewrite highlightLines' terminator-line check to the explicit
last !== undefined form to keep a single branch for per-file coverage.
Add the running-state sweep animation to ReadRow, matching BashRow/ToolRow,
so a running read row shows executing feedback.
Use file_path (the real read tool schema field) in the turn 66 read fixture
sample, its presentCall branch, and the turn 64 run_code read sub-dispatches,
so the built-boot snapshot replays a production-shaped call and the details
panel shows the correct Input JSON.
Document why ReadBlock omits TerminalBlock's empty-window copy guard, and
correct the read-card-model {@link} and the turn 66 fixture comment.
This commit is contained in:
@@ -267,8 +267,8 @@ function buildAlphaLog(): SessionEvent[] {
|
|||||||
const turn = 64
|
const turn = 64
|
||||||
const callId = `fx-call-${turn}`
|
const callId = `fx-call-${turn}`
|
||||||
const program = 'const listing = await tools.bash({ command: "ls notes", description: "List notes" })\n'
|
const program = 'const listing = await tools.bash({ command: "ls notes", description: "List notes" })\n'
|
||||||
+ 'const demo = await tools.read({ path: "notes/demo.txt" })\n'
|
+ 'const demo = await tools.read({ file_path: "notes/demo.txt" })\n'
|
||||||
+ 'await tools.read({ path: "notes/missing.txt" }).catch(() => "tolerated")\n'
|
+ 'await tools.read({ file_path: "notes/missing.txt" }).catch(() => "tolerated")\n'
|
||||||
+ 'return { listing, demo }'
|
+ 'return { listing, demo }'
|
||||||
const args = JSON.stringify({ code: program, description: 'Read the notes files and summarize' })
|
const args = JSON.stringify({ code: program, description: 'Read the notes files and summarize' })
|
||||||
push({ type: 'turn/start', data: { turn, trigger: { kind: 'message', source: { kind: 'user' } } } })
|
push({ type: 'turn/start', data: { turn, trigger: { kind: 'message', source: { kind: 'user' } } } })
|
||||||
@@ -293,8 +293,8 @@ function buildAlphaLog(): SessionEvent[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
dispatchPair(1, 'bash', { command: 'ls notes', description: 'List notes' }, 'demo.txt\nnew-demo.txt')
|
dispatchPair(1, 'bash', { command: 'ls notes', description: 'List notes' }, 'demo.txt\nnew-demo.txt')
|
||||||
dispatchPair(2, 'read', { path: 'notes/demo.txt' }, 'hello fixture\n')
|
dispatchPair(2, 'read', { file_path: 'notes/demo.txt' }, 'hello fixture\n')
|
||||||
dispatchPair(3, 'read', { path: 'notes/missing.txt' }, 'Error: ENOENT: notes/missing.txt not found', true)
|
dispatchPair(3, 'read', { file_path: 'notes/missing.txt' }, 'Error: ENOENT: notes/missing.txt not found', true)
|
||||||
push({
|
push({
|
||||||
type: 'tool/result', surfaceOp: 'append',
|
type: 'tool/result', surfaceOp: 'append',
|
||||||
data: { turn, step: 0, message: toolResultMessage(callId, text('{"listing":"demo.txt\\nnew-demo.txt","demo":"hello fixture\\n"}'), false) },
|
data: { turn, step: 0, message: toolResultMessage(callId, text('{"listing":"demo.txt\\nnew-demo.txt","demo":"hello fixture\\n"}'), false) },
|
||||||
@@ -326,13 +326,16 @@ function buildAlphaLog(): SessionEvent[] {
|
|||||||
// Turn 66: the read sample — a WINDOW past an offset so the card draws file
|
// Turn 66: the read sample — a WINDOW past an offset so the card draws file
|
||||||
// line numbers starting above 1 and a "showing N of M" note (the window is
|
// line numbers starting above 1 and a "showing N of M" note (the window is
|
||||||
// shorter than READ_SAMPLE_TOTAL), with a `ts` language hint the shiki path
|
// shorter than READ_SAMPLE_TOTAL), with a `ts` language hint the shiki path
|
||||||
// highlights. Named `read`, so it exercises the keyed ReadRow registration
|
// highlights. Named `read`, so it exercises the keyed ReadRow registration.
|
||||||
// (the render-site fallback row is covered by the read sub-dispatches in the
|
// The render-site fallback ROW SHAPE (a read call on the generic flattened
|
||||||
// turn 64 run_code sample). The read render intent is result-side only, so its
|
// path) is covered by the turn 64 run_code read sub-dispatches, which
|
||||||
// pending call stays a generic `kind: 'read'` card; presentResult carries the
|
// session.ts folds with resultView: null; the fallback-row + read-CARD
|
||||||
|
// combination is pinned by the web_fetch case in read-card.spec.tsx, not by
|
||||||
|
// this fixture. The read render intent is result-side only, so its pending
|
||||||
|
// call stays a generic `kind: 'read'` card; presentResult carries the
|
||||||
// structured window. Ordered BEFORE the todo turn for the same reason the
|
// structured window. Ordered BEFORE the todo turn for the same reason the
|
||||||
// terminal sample is: the standing plan retires at the next `turn/start`.
|
// terminal sample is: the standing plan retires at the next `turn/start`.
|
||||||
toolTurn(66, 'read', `{"path":${JSON.stringify(READ_SAMPLE_PATH)},"offset":${READ_SAMPLE_FIRST_LINE}}`, READ_SAMPLE_TEXT)
|
toolTurn(66, 'read', `{"file_path":${JSON.stringify(READ_SAMPLE_PATH)},"offset":${READ_SAMPLE_FIRST_LINE}}`, READ_SAMPLE_TEXT)
|
||||||
|
|
||||||
const todoArgs = JSON.stringify({ todos: fixtureTodos })
|
const todoArgs = JSON.stringify({ todos: fixtureTodos })
|
||||||
toolTurn(67, 'todo_write', todoArgs, 'Updated todo list: 1 pending, 1 in progress, 1 completed.')
|
toolTurn(67, 'todo_write', todoArgs, 'Updated todo list: 1 pending, 1 in progress, 1 completed.')
|
||||||
@@ -375,7 +378,7 @@ function presentCall(name: string, argsRaw: string): ToolCallView | undefined {
|
|||||||
// carries no file content until execute returns. The rich read card arrives
|
// carries no file content until execute returns. The rich read card arrives
|
||||||
// in presentResult.
|
// in presentResult.
|
||||||
case 'read':
|
case 'read':
|
||||||
return { card: 'generic', title: `Read ${str(args.path)}`, kind: 'read', locations: [{ path: str(args.path) }] }
|
return { card: 'generic', title: `Read ${str(args.file_path)}`, kind: 'read', locations: [{ path: str(args.file_path) }] }
|
||||||
case 'edit':
|
case 'edit':
|
||||||
return { card: 'generic', title: `Edit ${str(args.file_path)}`, kind: 'edit', rawInput: args }
|
return { card: 'generic', title: `Edit ${str(args.file_path)}`, kind: 'edit', rawInput: args }
|
||||||
case 'write':
|
case 'write':
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ import { relativizeToCwd, type ToolCallBlock } from './tool-call-model.ts'
|
|||||||
* scannable across many calls, while the details panel is the single-call
|
* scannable across many calls, while the details panel is the single-call
|
||||||
* reading surface. A design constant of this UI's row geometry, not a
|
* reading surface. A design constant of this UI's row geometry, not a
|
||||||
* deployment choice, so it is fixed here rather than a plugin Config field. The
|
* deployment choice, so it is fixed here rather than a plugin Config field. The
|
||||||
* same split {@link CHAT_TERMINAL_MAX_LINES} draws for terminal output.
|
* same split [`CHAT_TERMINAL_MAX_LINES`](./terminal-card-model.ts) draws for
|
||||||
|
* terminal output.
|
||||||
*/
|
*/
|
||||||
export const CHAT_READ_MAX_LINES = 8
|
export const CHAT_READ_MAX_LINES = 8
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
position: relative;
|
position: relative; /* sweep-glare overlay anchor */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -23,6 +23,32 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Running sweep glare — same pattern as BashRow/ToolRow, so a running read row
|
||||||
|
gives the same executing feedback a running command row does. The leading
|
||||||
|
read icon stays static (a read has no per-step state to animate); the sweep
|
||||||
|
is the row-level running signal. */
|
||||||
|
.root[data-state='running']::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 300px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
animation: dsh-read-row-sweep 2.6s ease-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dsh-read-row-sweep {
|
||||||
|
0% { left: -300px; }
|
||||||
|
90%, 100% { left: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
.leading {
|
.leading {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
|||||||
@@ -131,6 +131,13 @@ export function ReadBlock({
|
|||||||
<span className={css.count}>{`显示 ${lines.length} / ${totalLines} 行`}</span>
|
<span className={css.count}>{`显示 ${lines.length} / ${totalLines} 行`}</span>
|
||||||
)}
|
)}
|
||||||
<span className={css.lang}>{lang ?? ''}</span>
|
<span className={css.lang}>{lang ?? ''}</span>
|
||||||
|
{/* No empty-window guard around the copy control, unlike TerminalBlock
|
||||||
|
(which hides copy on empty output): a read card is reached only for
|
||||||
|
a settled read whose result view declares `card:'read'`, and the
|
||||||
|
read tool projects that view solely for a parsed envelope with a
|
||||||
|
line window. An empty or non-envelope result falls back to the
|
||||||
|
generic card upstream (readCardModel returns null), so `lines` is
|
||||||
|
never empty here — the branch TerminalBlock needs cannot arise. */}
|
||||||
<button type="button" className={css.copyButton} onClick={onCopy}>
|
<button type="button" className={css.copyButton} onClick={onCopy}>
|
||||||
{copied ? '复制成功' : '复制'}
|
{copied ? '复制成功' : '复制'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
* theme package's token sheets as `--shiki-*` custom properties (light and
|
* theme package's token sheets as `--shiki-*` custom properties (light and
|
||||||
* dark blocks), never here — the repo's tokens-only styling rule.
|
* dark blocks), never here — the repo's tokens-only styling rule.
|
||||||
*
|
*
|
||||||
* Grammars are the set the harness actually renders: TypeScript programs
|
* Grammars are the set the harness actually renders: the markdown-fence and
|
||||||
* (`run_code` bodies; TS pulls in JS via grammar embedding), shell commands,
|
* `run_code` languages (TypeScript, shell, JSON) plus the file-extension
|
||||||
* and JSON payloads. An unknown or absent language falls back to plain text
|
* language hints the read tool's `langFromPath` emits (`packages/fs/tool-fs`),
|
||||||
|
* so a read card highlights the same source, config, and markup extensions the
|
||||||
|
* backend recognizes. An unknown or absent language falls back to plain text
|
||||||
* (no highlighting, still monospace) — never an error.
|
* (no highlighting, still monospace) — never an error.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -16,14 +18,55 @@ import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
|
|||||||
import langTs from '@shikijs/langs/typescript'
|
import langTs from '@shikijs/langs/typescript'
|
||||||
import langBash from '@shikijs/langs/shellscript'
|
import langBash from '@shikijs/langs/shellscript'
|
||||||
import langJson from '@shikijs/langs/json'
|
import langJson from '@shikijs/langs/json'
|
||||||
|
import langPython from '@shikijs/langs/python'
|
||||||
|
import langRuby from '@shikijs/langs/ruby'
|
||||||
|
import langGo from '@shikijs/langs/go'
|
||||||
|
import langRust from '@shikijs/langs/rust'
|
||||||
|
import langJava from '@shikijs/langs/java'
|
||||||
|
import langC from '@shikijs/langs/c'
|
||||||
|
import langCpp from '@shikijs/langs/cpp'
|
||||||
|
import langCsharp from '@shikijs/langs/csharp'
|
||||||
|
import langKotlin from '@shikijs/langs/kotlin'
|
||||||
|
import langSwift from '@shikijs/langs/swift'
|
||||||
|
import langPhp from '@shikijs/langs/php'
|
||||||
|
import langYaml from '@shikijs/langs/yaml'
|
||||||
|
import langToml from '@shikijs/langs/toml'
|
||||||
|
import langIni from '@shikijs/langs/ini'
|
||||||
|
import langMarkdown from '@shikijs/langs/markdown'
|
||||||
|
import langMdx from '@shikijs/langs/mdx'
|
||||||
|
import langHtml from '@shikijs/langs/html'
|
||||||
|
import langCss from '@shikijs/langs/css'
|
||||||
|
import langScss from '@shikijs/langs/scss'
|
||||||
|
import langLess from '@shikijs/langs/less'
|
||||||
|
import langSql from '@shikijs/langs/sql'
|
||||||
|
import langXml from '@shikijs/langs/xml'
|
||||||
|
import langLua from '@shikijs/langs/lua'
|
||||||
import type { HighlighterCore } from 'shiki/core'
|
import type { HighlighterCore } from 'shiki/core'
|
||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grammars the singleton registers; each entry's own `name` is the id
|
||||||
|
* `codeToTokens`/`codeToHtml` resolve. The TypeScript grammar embeds JS/JSX/TSX,
|
||||||
|
* so the JS-family fence aliases resolve to it rather than a separate grammar.
|
||||||
|
*/
|
||||||
|
const LANGS = [
|
||||||
|
langTs, langBash, langJson,
|
||||||
|
langPython, langRuby, langGo, langRust, langJava,
|
||||||
|
langC, langCpp, langCsharp, langKotlin, langSwift, langPhp,
|
||||||
|
langYaml, langToml, langIni,
|
||||||
|
langMarkdown, langMdx, langHtml, langCss, langScss, langLess,
|
||||||
|
langSql, langXml, langLua,
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Language ids (and aliases) the singleton registers; everything else renders
|
* Language ids (and aliases) the singleton registers; everything else renders
|
||||||
* plain. A Map, not an object: fence info strings are assistant-authored, so
|
* plain. A Map, not an object: fence info strings are assistant-authored, so
|
||||||
* a label like `constructor` or `__proto__` must miss instead of resolving an
|
* a label like `constructor` or `__proto__` must miss instead of resolving an
|
||||||
* inherited property and crashing the renderer inside shiki.
|
* inherited property and crashing the renderer inside shiki. Keys cover both
|
||||||
|
* the markdown-fence aliases `CodeBlock` uses and the file-extension hint ids
|
||||||
|
* the read tool's `langFromPath` emits, so both callers resolve the same
|
||||||
|
* grammars. The JS family maps to the TypeScript grammar (which embeds it),
|
||||||
|
* unchanged from when this was the only non-shell/JSON grammar.
|
||||||
*/
|
*/
|
||||||
const LANG_ALIASES = new Map<string, string>([
|
const LANG_ALIASES = new Map<string, string>([
|
||||||
['typescript', 'typescript'],
|
['typescript', 'typescript'],
|
||||||
@@ -31,6 +74,7 @@ const LANG_ALIASES = new Map<string, string>([
|
|||||||
['tsx', 'typescript'],
|
['tsx', 'typescript'],
|
||||||
['javascript', 'typescript'],
|
['javascript', 'typescript'],
|
||||||
['js', 'typescript'],
|
['js', 'typescript'],
|
||||||
|
['jsx', 'typescript'],
|
||||||
['shellscript', 'shellscript'],
|
['shellscript', 'shellscript'],
|
||||||
['bash', 'shellscript'],
|
['bash', 'shellscript'],
|
||||||
['sh', 'shellscript'],
|
['sh', 'shellscript'],
|
||||||
@@ -38,6 +82,35 @@ const LANG_ALIASES = new Map<string, string>([
|
|||||||
['zsh', 'shellscript'],
|
['zsh', 'shellscript'],
|
||||||
['json', 'json'],
|
['json', 'json'],
|
||||||
['jsonc', 'json'],
|
['jsonc', 'json'],
|
||||||
|
['py', 'python'],
|
||||||
|
['python', 'python'],
|
||||||
|
['rb', 'ruby'],
|
||||||
|
['ruby', 'ruby'],
|
||||||
|
['go', 'go'],
|
||||||
|
['rs', 'rust'],
|
||||||
|
['rust', 'rust'],
|
||||||
|
['java', 'java'],
|
||||||
|
['c', 'c'],
|
||||||
|
['cpp', 'cpp'],
|
||||||
|
['cs', 'csharp'],
|
||||||
|
['csharp', 'csharp'],
|
||||||
|
['kotlin', 'kotlin'],
|
||||||
|
['swift', 'swift'],
|
||||||
|
['php', 'php'],
|
||||||
|
['yaml', 'yaml'],
|
||||||
|
['yml', 'yaml'],
|
||||||
|
['toml', 'toml'],
|
||||||
|
['ini', 'ini'],
|
||||||
|
['md', 'markdown'],
|
||||||
|
['markdown', 'markdown'],
|
||||||
|
['mdx', 'mdx'],
|
||||||
|
['html', 'html'],
|
||||||
|
['css', 'css'],
|
||||||
|
['scss', 'scss'],
|
||||||
|
['less', 'less'],
|
||||||
|
['sql', 'sql'],
|
||||||
|
['xml', 'xml'],
|
||||||
|
['lua', 'lua'],
|
||||||
])
|
])
|
||||||
|
|
||||||
/** All token colors resolve through `--shiki-*` custom properties (theme package sheets). */
|
/** All token colors resolve through `--shiki-*` custom properties (theme package sheets). */
|
||||||
@@ -53,7 +126,7 @@ let singleton: HighlighterCore | undefined
|
|||||||
function highlighter(): HighlighterCore {
|
function highlighter(): HighlighterCore {
|
||||||
singleton ??= createHighlighterCoreSync({
|
singleton ??= createHighlighterCoreSync({
|
||||||
themes: [cssVariablesTheme],
|
themes: [cssVariablesTheme],
|
||||||
langs: [langTs, langBash, langJson],
|
langs: LANGS,
|
||||||
engine: createJavaScriptRegexEngine({ forgiving: true }),
|
engine: createJavaScriptRegexEngine({ forgiving: true }),
|
||||||
})
|
})
|
||||||
return singleton
|
return singleton
|
||||||
@@ -114,8 +187,11 @@ export function highlightLines(code: string, lang: string | undefined): Highligh
|
|||||||
const { tokens } = highlighter().codeToTokens(code, { lang: resolved, theme: 'css-variables' })
|
const { tokens } = highlighter().codeToTokens(code, { lang: resolved, theme: 'css-variables' })
|
||||||
// shiki tokenizes `a\nb` into two lines; a trailing newline (`a\n`) adds a
|
// shiki tokenizes `a\nb` into two lines; a trailing newline (`a\n`) adds a
|
||||||
// third, empty line the caller's own line array does not carry. Drop that
|
// third, empty line the caller's own line array does not carry. Drop that
|
||||||
// one terminator line so the two structures stay in step.
|
// one terminator line so the two structures stay in step. The explicit
|
||||||
const lines = tokens.length > 1 && tokens[tokens.length - 1]?.length === 0
|
// `last !== undefined` (over `tokens[...]?.length`) keeps a single branch for
|
||||||
|
// per-file coverage, matching TerminalBlock's terminator check.
|
||||||
|
const last = tokens[tokens.length - 1]
|
||||||
|
const lines = tokens.length > 1 && last !== undefined && last.length === 0
|
||||||
? tokens.slice(0, -1)
|
? tokens.slice(0, -1)
|
||||||
: tokens
|
: tokens
|
||||||
return lines.map(line => line.map(token => ({ text: token.content, style: { color: token.color } })))
|
return lines.map(line => line.map(token => ({ text: token.content, style: { color: token.color } })))
|
||||||
|
|||||||
Reference in New Issue
Block a user