@spexcode/transcript-ui 0.7.0-next.5 → 0.7.0-next.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ToolLine.js CHANGED
@@ -28,8 +28,9 @@ function WithheldOutput({ tool }) {
28
28
  }
29
29
  // One tool call as a SENTENCE, not a card: verb, target, and the size of what came back. It is
30
30
  // `inline-flex` so a dozen of them read as a list of things that happened rather than a dozen boxes. There
31
- // is no success mark, because the transcript carries no per-tool status — the past-tense verb is the whole
32
- // claim. A running call wears a small spinner and the word.
31
+ // is no success mark — the past-tense verb is the whole claim. A running call wears a small spinner and the
32
+ // word; a call whose harness recorded a structured failure wears `failed`, and one the person refused wears
33
+ // `rejected` — the outcome is the transcript's own field ([[transcript-reader]]), never read off the output prose.
33
34
  export function ToolLine({ tool, open, onToggle, live = false }) {
34
35
  const { labels, vocabulary } = useTranscriptUi();
35
36
  const target = toolTarget(tool.input, vocabulary);
@@ -38,8 +39,9 @@ export function ToolLine({ tool, open, onToggle, live = false }) {
38
39
  const withheld = tool.output === null;
39
40
  const canOpen = !!tool.input || tool.output !== undefined || withheld;
40
41
  const running = isRunning(tool, live);
41
- const row = (_jsxs(_Fragment, { children: [_jsx("span", { className: "tx-tool-verb", children: toolVerb(tool.name, vocabulary) }), lead && _jsx("span", { className: "tx-tool-target", children: lead }), trail && _jsx("span", { className: "tx-tool-trail", children: trail }), lines > 0 && _jsx("span", { className: "tx-tool-size", children: labels.lines(lines) }), running && _jsxs("span", { className: "tx-tool-running", children: [_jsx(Spinner, {}), labels.running] }), canOpen && _jsx(Caret, { open: open, className: "tx-tool-caret" })] }));
42
- return (_jsxs("div", { className: `tx-tool${running ? ' is-running' : ''}`, children: [canOpen
42
+ const outcome = tool.outcome;
43
+ const row = (_jsxs(_Fragment, { children: [_jsx("span", { className: "tx-tool-verb", children: toolVerb(tool.name, vocabulary) }), lead && _jsx("span", { className: "tx-tool-target", children: lead }), trail && _jsx("span", { className: "tx-tool-trail", children: trail }), lines > 0 && _jsx("span", { className: "tx-tool-size", children: labels.lines(lines) }), running && _jsxs("span", { className: "tx-tool-running", children: [_jsx(Spinner, {}), labels.running] }), outcome && _jsx("span", { className: `tx-tool-outcome is-${outcome}`, children: outcome === 'failed' ? labels.failed : labels.rejected }), canOpen && _jsx(Caret, { open: open, className: "tx-tool-caret" })] }));
44
+ return (_jsxs("div", { className: `tx-tool${running ? ' is-running' : ''}${outcome ? ` is-${outcome}` : ''}`, children: [canOpen
43
45
  ? _jsx("button", { type: "button", onClick: onToggle, "aria-expanded": open, className: "tx-tool-row is-openable", children: row })
44
46
  : _jsx("div", { className: "tx-tool-row", children: row }), open && canOpen && _jsxs(_Fragment, { children: [tool.input && _jsx("pre", { className: "tx-tool-in", children: tool.input }), withheld
45
47
  ? _jsx(WithheldOutput, { tool: tool })
@@ -58,5 +60,7 @@ export function ToolRun({ tools, openIds, onToggle, live = false, fold = true })
58
60
  const id = `run:${tools[0].id}`;
59
61
  const open = openIds.has(id);
60
62
  const running = tools.some((tool) => isRunning(tool, live));
61
- return _jsx("div", { className: "tx-tools", children: _jsxs("div", { className: `tx-tool${running ? ' is-running' : ''}`, children: [_jsxs("button", { type: "button", className: "tx-tool-row is-openable is-run", "aria-expanded": open, onClick: () => onToggle(id), children: [_jsx("span", { className: "tx-tool-verb", children: labels.toolUses(tools.length) }), _jsx("span", { className: "tx-tool-trail", children: runKinds(tools, vocabulary) }), _jsx(Caret, { open: open, className: "tx-tool-caret" })] }), open && _jsx("div", { className: "tx-tool-kids", children: tools.map(line) })] }) });
63
+ // a fold must not hide a failure: the row counts the calls that did not succeed
64
+ const failed = tools.filter((tool) => tool.outcome).length;
65
+ return _jsx("div", { className: "tx-tools", children: _jsxs("div", { className: `tx-tool${running ? ' is-running' : ''}${failed ? ' is-failed' : ''}`, children: [_jsxs("button", { type: "button", className: "tx-tool-row is-openable is-run", "aria-expanded": open, onClick: () => onToggle(id), children: [_jsx("span", { className: "tx-tool-verb", children: labels.toolUses(tools.length) }), _jsx("span", { className: "tx-tool-trail", children: runKinds(tools, vocabulary) }), failed > 0 && _jsx("span", { className: "tx-tool-outcome is-failed", children: labels.failedCount(failed) }), _jsx(Caret, { open: open, className: "tx-tool-caret" })] }), open && _jsx("div", { className: "tx-tool-kids", children: tools.map(line) })] }) });
62
66
  }
@@ -8,6 +8,9 @@ export declare function TurnBody({ turn, openIds, onToggle, live, fold }: {
8
8
  live: boolean;
9
9
  fold?: boolean;
10
10
  } & Disclosure): import("react").JSX.Element;
11
+ export declare function QuotedTurn({ turn }: {
12
+ turn: AnyTurn;
13
+ }): import("react").JSX.Element;
11
14
  export declare function WorkSegmentView({ segment, openIds, onToggle, live }: {
12
15
  segment: Work;
13
16
  live: boolean;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useTranscriptUi } from './context.js';
3
3
  import { Caret } from './icons.js';
4
4
  import { Quote } from './Quote.js';
5
+ import { parseEnvelope } from './envelope.js';
5
6
  import { segments } from './segments.js';
6
7
  import { ToolRun } from './ToolLine.js';
7
8
  import { useDisclosure } from './useDisclosure.js';
@@ -11,19 +12,27 @@ export function TurnBody({ turn, openIds, onToggle, live, fold = true }) {
11
12
  const { renderText } = useTranscriptUi();
12
13
  return _jsxs("div", { className: "tx-say", children: [turn.text && _jsx("div", { className: "tx-say-text", children: renderText(turn.text) }), _jsx(ToolRun, { tools: turn.tools, openIds: openIds, onToggle: onToggle, live: live, fold: fold })] });
13
14
  }
15
+ // a quoted turn is read through the envelope rows: the sender it names, the body it carried
16
+ export function QuotedTurn({ turn }) {
17
+ const { envelopes } = useTranscriptUi();
18
+ const envelope = parseEnvelope(turn.text || '', envelopes);
19
+ return _jsx(Quote, { who: envelope.who, ts: envelope.at ?? turn.at, text: envelope.body, className: "tx-quote-nested" });
20
+ }
14
21
  export function WorkSegmentView({ segment, openIds, onToggle, live }) {
15
22
  const { labels, vocabulary } = useTranscriptUi();
16
23
  const id = `seg:${segment.work[0]?.id || segment.answer?.id}`;
17
24
  const open = openIds.has(id);
18
25
  const kinds = runKinds(segment.work.flatMap((turn) => turn.tools ?? []), vocabulary);
19
26
  const foldedCalls = segment.work.reduce((n, turn) => n + (turn.tools?.length || 0), 0);
27
+ // a fold must not hide a failure: the row counts the calls whose harness recorded one
28
+ const failedCalls = segment.work.reduce((n, turn) => n + (turn.tools?.filter((tool) => tool.outcome).length || 0), 0);
20
29
  // history folds its runs; the work in progress (a live segment's calls after its newest prose) does not
21
30
  const history = !segment.now || !!segment.answer;
22
- return _jsxs(_Fragment, { children: [segment.folded ? (_jsxs("div", { className: "tx-work", children: [_jsxs("button", { type: "button", className: "tx-work-row", "aria-expanded": open, onClick: () => onToggle(id), children: [_jsx("span", { className: "tx-work-lead", children: labels.toolUses(foldedCalls) }), kinds && _jsx("span", { className: "tx-work-detail", children: kinds }), _jsx(Caret, { open: open, className: "tx-work-caret" })] }), open && _jsx("div", { className: "tx-work-body", children: segment.work.map((turn) => _jsx(TurnBody, { turn: turn, openIds: openIds, onToggle: onToggle, live: live }, turn.id)) })] })) : segment.work.map((turn) => _jsx(TurnBody, { turn: turn, openIds: openIds, onToggle: onToggle, live: live, fold: history }, turn.id)), segment.answer && _jsx(TurnBody, { turn: segment.answer, openIds: openIds, onToggle: onToggle, live: live, fold: !segment.now }), segment.after.map((turn) => _jsx(TurnBody, { turn: turn, openIds: openIds, onToggle: onToggle, live: live, fold: !segment.now }, turn.id))] });
31
+ return _jsxs(_Fragment, { children: [segment.folded ? (_jsxs("div", { className: `tx-work${failedCalls ? ' is-failed' : ''}`, children: [_jsxs("button", { type: "button", className: "tx-work-row", "aria-expanded": open, onClick: () => onToggle(id), children: [_jsx("span", { className: "tx-work-lead", children: labels.toolUses(foldedCalls) }), kinds && _jsx("span", { className: "tx-work-detail", children: kinds }), failedCalls > 0 && _jsx("span", { className: "tx-tool-outcome is-failed", children: labels.failedCount(failedCalls) }), _jsx(Caret, { open: open, className: "tx-work-caret" })] }), open && _jsx("div", { className: "tx-work-body", children: segment.work.map((turn) => _jsx(TurnBody, { turn: turn, openIds: openIds, onToggle: onToggle, live: live }, turn.id)) })] })) : segment.work.map((turn) => _jsx(TurnBody, { turn: turn, openIds: openIds, onToggle: onToggle, live: live, fold: history }, turn.id)), segment.answer && _jsx(TurnBody, { turn: segment.answer, openIds: openIds, onToggle: onToggle, live: live, fold: !segment.now }), segment.after.map((turn) => _jsx(TurnBody, { turn: turn, openIds: openIds, onToggle: onToggle, live: live, fold: !segment.now }, turn.id))] });
23
32
  }
24
33
  export function SegmentView({ segment, ...rest }) {
25
34
  if (segment.kind === 'quote')
26
- return _jsx(Quote, { ts: segment.turn.at, text: segment.turn.text || '', className: "tx-quote-nested" });
35
+ return _jsx(QuotedTurn, { turn: segment.turn });
27
36
  return _jsx(WorkSegmentView, { segment: segment, ...rest });
28
37
  }
29
38
  // THE TURNS, in the grammar: quotes where the host wants them, work segments folded behind their answers
package/dist/context.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type Vocabulary } from './vocabulary.js';
3
3
  import type { FoldPolicy, UserTurnPolicy } from './segments.js';
4
+ import { type EnvelopeParser } from './envelope.js';
4
5
  export type ToolOutputResult = {
5
6
  ok: true;
6
7
  output: string | null;
@@ -11,6 +12,9 @@ export type ToolOutputResult = {
11
12
  export type Labels = Readonly<{
12
13
  loading: string;
13
14
  running: string;
15
+ failed: string;
16
+ rejected: string;
17
+ failedCount: (n: number) => string;
14
18
  more: string;
15
19
  toolUses: (n: number) => string;
16
20
  lines: (n: number) => string;
@@ -27,6 +31,7 @@ export type TranscriptUiOptions = Readonly<{
27
31
  loadToolOutput: ((toolId: string) => Promise<ToolOutputResult>) | null;
28
32
  labels: Labels;
29
33
  vocabulary: Vocabulary;
34
+ envelopes: readonly EnvelopeParser[];
30
35
  fold: FoldPolicy;
31
36
  runMin: number;
32
37
  userTurns: UserTurnPolicy;
package/dist/context.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext } from 'react';
3
3
  import { defaultVocabulary } from './vocabulary.js';
4
+ import { defaultEnvelopes } from './envelope.js';
4
5
  export const defaultLabels = {
5
6
  loading: 'loading…',
6
7
  running: 'running',
8
+ failed: 'failed',
9
+ rejected: 'rejected',
10
+ failedCount: (n) => `${n} failed`,
7
11
  more: 'more',
8
12
  toolUses: (n) => `${n} tool use${n === 1 ? '' : 's'}`,
9
13
  lines: (n) => `${n} line${n === 1 ? '' : 's'}`,
@@ -20,6 +24,7 @@ export const defaultOptions = {
20
24
  loadToolOutput: null,
21
25
  labels: defaultLabels,
22
26
  vocabulary: defaultVocabulary,
27
+ envelopes: defaultEnvelopes,
23
28
  fold: 'segments',
24
29
  runMin: 3,
25
30
  userTurns: 'boundary',
@@ -0,0 +1,10 @@
1
+ export type Envelope = Readonly<{
2
+ who: string | null;
3
+ id?: string | null;
4
+ at?: number | null;
5
+ body: string;
6
+ }>;
7
+ export type EnvelopeParser = (text: string) => Envelope | null;
8
+ export declare const spexEnvelope: EnvelopeParser;
9
+ export declare const defaultEnvelopes: readonly EnvelopeParser[];
10
+ export declare function parseEnvelope(text: string, parsers?: readonly EnvelopeParser[]): Envelope;
@@ -0,0 +1,18 @@
1
+ // `— from session "label" (id) on machine m. To reply: spex session send [--ssh addr] id "<your reply>"`
2
+ const SPEX_FOOTER = /\n*— from session (?:"(.*?)" \(([^\s)]+)\)|(\S+))(?: on machine \S+)?\. To reply: spex session send (?:--ssh \S+ )?\S+ "<your reply>"\s*$/;
3
+ export const spexEnvelope = (text) => {
4
+ const m = SPEX_FOOTER.exec(text || '');
5
+ if (!m)
6
+ return null;
7
+ const id = m[2] || m[3] || null;
8
+ return { who: m[1] || id, id, body: text.slice(0, m.index) };
9
+ };
10
+ export const defaultEnvelopes = [spexEnvelope];
11
+ export function parseEnvelope(text, parsers = defaultEnvelopes) {
12
+ for (const parse of parsers) {
13
+ const envelope = parse(text);
14
+ if (envelope)
15
+ return envelope;
16
+ }
17
+ return { who: null, body: text };
18
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './vocabulary.js';
2
+ export * from './envelope.js';
2
3
  export * from './segments.js';
3
4
  export * from './context.js';
4
5
  export * from './icons.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './vocabulary.js';
2
+ export * from './envelope.js';
2
3
  export * from './segments.js';
3
4
  export * from './context.js';
4
5
  export * from './icons.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/transcript-ui",
3
- "version": "0.7.0-next.5",
3
+ "version": "0.7.0-next.7",
4
4
  "type": "module",
5
5
  "description": "React components that draw a normalized agent transcript — the person quoted, the agent as the page, a tool call as a sentence, the work folded behind its answer — with the fold, the prose renderer, the tool vocabulary, the labels and the design tokens all tunable.",
6
6
  "files": [
@@ -24,7 +24,7 @@
24
24
  "test": "npm run build && tsx --test src/*.test.tsx"
25
25
  },
26
26
  "dependencies": {
27
- "@spexcode/transcript": "0.7.0-next.5"
27
+ "@spexcode/transcript": "0.7.0-next.7"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "react": "^18.2.0 || ^19.0.0"
package/styles.css CHANGED
@@ -104,6 +104,8 @@
104
104
  of words still being said. Reduced motion keeps the marks and drops the movement. */
105
105
  .tx-tool.is-running .tx-tool-verb { color: var(--tx-ink2); }
106
106
  .tx-tool-running { display: inline-flex; align-items: center; gap: 4px; flex: none; color: var(--tx-orange); font-size: var(--tx-type-caption); }
107
+ .tx-tool-outcome { flex: none; color: var(--tx-red); font-size: var(--tx-type-caption); }
108
+ .tx-tool.is-failed > .tx-tool-row .tx-tool-verb, .tx-tool.is-rejected > .tx-tool-row .tx-tool-verb { color: var(--tx-red); }
107
109
  .tx-spin { animation: tx-spin 1s linear infinite; }
108
110
  @keyframes tx-spin { to { transform: rotate(360deg); } }
109
111
  .tx-live { margin-top: var(--tx-space-2); min-width: 0; }