@textui/chat 0.6.0

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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/blocks.d.ts +75 -0
  4. package/dist/blocks.d.ts.map +1 -0
  5. package/dist/blocks.js +50 -0
  6. package/dist/bubble.d.ts +122 -0
  7. package/dist/bubble.d.ts.map +1 -0
  8. package/dist/bubble.js +108 -0
  9. package/dist/composer.d.ts +67 -0
  10. package/dist/composer.d.ts.map +1 -0
  11. package/dist/composer.js +194 -0
  12. package/dist/controls.d.ts +66 -0
  13. package/dist/controls.d.ts.map +1 -0
  14. package/dist/controls.js +77 -0
  15. package/dist/details.d.ts +66 -0
  16. package/dist/details.d.ts.map +1 -0
  17. package/dist/details.js +65 -0
  18. package/dist/diff.d.ts +45 -0
  19. package/dist/diff.d.ts.map +1 -0
  20. package/dist/diff.js +111 -0
  21. package/dist/filediff.d.ts +30 -0
  22. package/dist/filediff.d.ts.map +1 -0
  23. package/dist/filediff.js +24 -0
  24. package/dist/hitl.d.ts +85 -0
  25. package/dist/hitl.d.ts.map +1 -0
  26. package/dist/hitl.js +134 -0
  27. package/dist/icons.d.ts +14 -0
  28. package/dist/icons.d.ts.map +1 -0
  29. package/dist/icons.js +71 -0
  30. package/dist/index.d.ts +17 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +16 -0
  33. package/dist/measure.d.ts +14 -0
  34. package/dist/measure.d.ts.map +1 -0
  35. package/dist/measure.js +19 -0
  36. package/dist/picker.d.ts +43 -0
  37. package/dist/picker.d.ts.map +1 -0
  38. package/dist/picker.js +79 -0
  39. package/dist/sessionhead.d.ts +42 -0
  40. package/dist/sessionhead.d.ts.map +1 -0
  41. package/dist/sessionhead.js +58 -0
  42. package/dist/sessions.d.ts +35 -0
  43. package/dist/sessions.d.ts.map +1 -0
  44. package/dist/sessions.js +58 -0
  45. package/dist/toolcall.d.ts +28 -0
  46. package/dist/toolcall.d.ts.map +1 -0
  47. package/dist/toolcall.js +54 -0
  48. package/dist/transcript.d.ts +53 -0
  49. package/dist/transcript.d.ts.map +1 -0
  50. package/dist/transcript.js +67 -0
  51. package/dist/types.d.ts +176 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +11 -0
  54. package/package.json +58 -0
  55. package/src/blocks.ts +73 -0
  56. package/src/bubble.tsx +266 -0
  57. package/src/composer.tsx +302 -0
  58. package/src/controls.tsx +222 -0
  59. package/src/details.tsx +162 -0
  60. package/src/diff.ts +132 -0
  61. package/src/filediff.tsx +118 -0
  62. package/src/hitl.tsx +392 -0
  63. package/src/icons.ts +109 -0
  64. package/src/index.ts +16 -0
  65. package/src/measure.ts +21 -0
  66. package/src/picker.ts +105 -0
  67. package/src/sessionhead.tsx +105 -0
  68. package/src/sessions.tsx +146 -0
  69. package/src/toolcall.tsx +136 -0
  70. package/src/transcript.tsx +221 -0
  71. package/src/types.ts +171 -0
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
2
+ import { defineComponent, useTheme } from '@textui/core';
3
+ import { Column, KeyValue, Row } from '@textui/widgets';
4
+ /**
5
+ * A timestamp a person can read, in whatever this machine calls a date.
6
+ *
7
+ * The host sends ISO, which is unambiguous and not what anybody wants to read
8
+ * off a caption. An unparseable one is passed through rather than shown as
9
+ * "Invalid Date": what the host said is more useful than what we made of it.
10
+ */
11
+ function when(iso) {
12
+ if (!iso)
13
+ return '';
14
+ const at = new Date(iso);
15
+ if (Number.isNaN(at.getTime()))
16
+ return iso;
17
+ return at.toLocaleString(undefined, {
18
+ year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
19
+ });
20
+ }
21
+ export const ChatSessionHead = defineComponent('ChatSessionHead', (props) => {
22
+ const { session, model, chat, settings = [], present = [], ...rest } = props;
23
+ const theme = useTheme();
24
+ const status = session.status;
25
+ const started = when(session.createdAt);
26
+ const updated = when(session.modifiedAt);
27
+ // Only what is known. A row of empty values is what building this from a
28
+ // fixed list produces, and it reads as a session the host would not talk
29
+ // about rather than as one nobody has asked yet.
30
+ const rows = [
31
+ { label: 'Harness', value: [session.provider, model].filter(Boolean).join(` ${theme.glyphs.separator} `) },
32
+ ...settings.filter((setting) => setting.value).map((setting) => ({ ...setting })),
33
+ { label: 'Workspace', value: session.workingDirectories.map((dir) => dir.replace(/^file:\/\//, '')).join(', ') },
34
+ // Only when the host says one. A blank branch row reads as a detached
35
+ // head rather than as a host that does not report branches.
36
+ ...(session.branch ? [{ label: 'Branch', value: session.branch }] : []),
37
+ {
38
+ label: 'Started',
39
+ value: started && updated && updated !== started
40
+ ? `${started} ${theme.glyphs.separator} updated ${updated}`
41
+ : started,
42
+ },
43
+ // Last, and in full. A uri you can read half of is worse than one you
44
+ // cannot see at all: it looks like the whole thing.
45
+ { label: 'Session', value: session.id },
46
+ ...(chat ? [{ label: 'Chat', value: chat }] : []),
47
+ // Only when somebody else is here. One entry is this client, and a row
48
+ // saying you are the person reading it is a row that tells nobody
49
+ // anything.
50
+ ...(present.length > 1
51
+ ? [{
52
+ label: 'Here',
53
+ value: present.map((one) => one.displayName ?? one.clientId).join(` ${theme.glyphs.separator} `),
54
+ }]
55
+ : []),
56
+ ].filter((row) => row.value !== '');
57
+ return (_jsxs(Column, { ...rest, gap: 0, children: [_jsxs(Row, { gap: 1, children: [_jsx("text", { content: theme.glyphs[status.glyph], fg: status.tone, shrink: 0 }), _jsx("text", { content: session.title, bold: true, wrap: "word", flex: 1 }), _jsx("text", { content: status.label, fg: status.tone, shrink: 0 })] }), _jsx(KeyValue, { items: rows })] }));
58
+ });
@@ -0,0 +1,35 @@
1
+ import type { BoxProps, RenderOutput } from '@textui/core';
2
+ import type { ChatSession } from './types.js';
3
+ /**
4
+ * The catalogue.
5
+ *
6
+ * Still a `List` - the selection, the keys, the window and the highlight are
7
+ * all the list's, and reimplementing them here is what the transcript already
8
+ * proved is a mistake. What is ours is the row, because a session does not fit
9
+ * the one-line shape a list gives you for free.
10
+ *
11
+ * It takes two lines, and the first one is why. A title, a harness, a
12
+ * workspace and a status sharing a pane that is also sharing the terminal
13
+ * with the detail panel leaves every one of them truncated:
14
+ * `Draft replies for desk-produ…` beside `1b444e78-d050-4fb5-a5…` names
15
+ * neither the conversation nor the directory it is in. So the title gets the
16
+ * width, and everything that qualifies it goes underneath.
17
+ */
18
+ export interface SessionListProps extends BoxProps {
19
+ sessions: ChatSession[];
20
+ selectedId?: string | null;
21
+ onSelect?(id: string): void;
22
+ onOpen?(id: string): void;
23
+ emptyMessage?: string;
24
+ focusId?: string;
25
+ autoFocus?: boolean;
26
+ }
27
+ export declare const SessionList: (props: SessionListProps) => RenderOutput;
28
+ export interface ConnectionBadgeProps extends BoxProps {
29
+ url: string;
30
+ state: 'connecting' | 'connected' | 'offline';
31
+ sessions?: number;
32
+ }
33
+ /** Which host, and whether it is answering. */
34
+ export declare const ConnectionBadge: (props: ConnectionBadgeProps) => RenderOutput;
35
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAmB,MAAM,cAAc,CAAC;AAI5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,gBAAiB,SAAQ,QAAQ;IAChD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,eAAO,MAAM,WAAW,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,YAuFnD,CAAC;AAEL,MAAM,WAAW,oBAAqB,SAAQ,QAAQ;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,YAAY,GAAG,WAAW,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,+CAA+C;AAC/C,eAAO,MAAM,eAAe,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,YAiB3D,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
2
+ import { defineComponent, stringWidth, useTheme } from '@textui/core';
3
+ import { Badge, Column, List, Marquee, Row } from '@textui/widgets';
4
+ export const SessionList = defineComponent('SessionList', (props) => {
5
+ const { sessions, selectedId, onSelect, onOpen, emptyMessage, focusId, autoFocus, ...rest } = props;
6
+ const theme = useTheme();
7
+ const dot = ` ${theme.glyphs.separator} `;
8
+ const items = sessions.map((session) => {
9
+ const status = session.status;
10
+ const changes = session.changes;
11
+ return {
12
+ id: session.id,
13
+ // Glyph first, so the one that wants a person is findable in a piped
14
+ // log, a 16-colour session and by a reader who cannot see the colour.
15
+ icon: theme.glyphs[status.glyph],
16
+ label: session.title,
17
+ // The second line, in the order it gets read: which harness, then
18
+ // where, then what it has to show for it.
19
+ //
20
+ // The harness stands where a model would: a catalogue row does not
21
+ // carry one, and a model per row would be a subscription per row.
22
+ description: [
23
+ session.provider,
24
+ // The project, then the branch it is on - a catalogue spanning
25
+ // several repositories is read by which one each row is in, and a
26
+ // list of them all on `main` is a list that needs opening to tell
27
+ // apart. Then the pull request the branch became, where the host
28
+ // found one: a merged branch is a finished row.
29
+ [session.project, session.branch, session.pullRequest].filter(Boolean).join(' '),
30
+ changes?.files
31
+ ? `${changes.files} files +${changes.additions ?? 0} -${changes.deletions ?? 0}`
32
+ : '',
33
+ // What the host says it is doing, in its own words. Last, because it
34
+ // is the one that is usually not there.
35
+ session.activity ?? '',
36
+ // Why it is here at all, when nobody started it. Without this a
37
+ // session that appeared at nine in the morning is a row with no
38
+ // account of itself, sitting among rows somebody typed.
39
+ session.origin ?? '',
40
+ status.archived ? 'archived' : '',
41
+ ].filter(Boolean).join(dot),
42
+ meta: status.label,
43
+ tone: status.tone,
44
+ };
45
+ });
46
+ return (_jsx(List, { items: items, itemHeight: 2, renderItem: (item, state) => (_jsxs(Column, { children: [_jsxs(Row, { gap: 1, children: [_jsx("text", { content: item.icon ?? '', ...(state.selected ? {} : { fg: item.tone }), shrink: 0 }), _jsx(Marquee, { content: item.label, active: state.selected && state.focused, flex: 1 }), _jsx("text", { content: item.meta ?? '', ...(state.selected ? {} : { fg: 'muted' }), shrink: 0 })] }), _jsxs(Row, { children: [_jsx("text", { content: ' '.repeat(stringWidth(item.icon ?? '') + 1), shrink: 0 }), _jsx(Marquee, { content: item.description ?? '', active: state.selected && state.focused, ...(state.selected ? {} : { fg: 'muted' }), flex: 1 })] })] })), ...(selectedId ? { selectedId } : {}), emptyMessage: emptyMessage ?? 'No sessions on this host', onSelect: (id) => onSelect?.(id), onActivate: (id) => onOpen?.(id), ...(focusId ? { focusId } : {}), ...(autoFocus ? { autoFocus: true } : {}), ...rest }));
47
+ });
48
+ /** Which host, and whether it is answering. */
49
+ export const ConnectionBadge = defineComponent('ConnectionBadge', (props) => {
50
+ const { url, state, sessions, ...rest } = props;
51
+ const theme = useTheme();
52
+ const look = {
53
+ connected: { tone: 'success', glyph: theme.glyphs.bulletFilled },
54
+ connecting: { tone: 'warning', glyph: theme.glyphs.bulletHalf },
55
+ offline: { tone: 'danger', glyph: theme.glyphs.cross },
56
+ }[state];
57
+ return (_jsxs(Row, { gap: 1, ...rest, children: [_jsx("text", { content: look.glyph, fg: look.tone }), _jsx("text", { content: url, fg: "muted", truncate: "start" }), sessions !== undefined ? _jsx(Badge, { label: `${sessions} sessions`, tone: "muted" }) : null] }));
58
+ });
@@ -0,0 +1,28 @@
1
+ import type { BoxProps, RenderOutput } from '@textui/core';
2
+ import type { ChatToolCall } from './types.js';
3
+ /**
4
+ * A tool call, as a row.
5
+ *
6
+ * Twenty of these in a turn look identical unless the *command* is on the row,
7
+ * so the input is the row and the display name is a prefix. What it meant to
8
+ * do is markdown, like everything else a host writes for a person; what came
9
+ * back is not, and is shown as it arrived.
10
+ *
11
+ * The status is a glyph and a colour together. A 16-colour session, a piped
12
+ * log and a colourblind reader all lose the colour and keep the glyph.
13
+ *
14
+ * In the transcript it sits beside a blank gutter rather than inside the rule,
15
+ * with its status glyph where the header's bullet is. A tool call is something
16
+ * the agent *did*; indenting it inside the rule filed it under what the agent
17
+ * was saying, which is the one thing it is not.
18
+ */
19
+ export interface ToolCallRowProps extends BoxProps {
20
+ call: ChatToolCall;
21
+ expanded?: boolean;
22
+ /** The transcript's cursor is on this row. */
23
+ active?: boolean;
24
+ /** Clicking the row opens it. */
25
+ onToggle?(): void;
26
+ }
27
+ export declare const ToolCallRow: (props: ToolCallRowProps) => RenderOutput;
28
+ //# sourceMappingURL=toolcall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolcall.d.ts","sourceRoot":"","sources":["../src/toolcall.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAmB,MAAM,cAAc,CAAC;AAG5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,gBAAiB,SAAQ,QAAQ;IAChD,IAAI,EAAE,YAAY,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iCAAiC;IACjC,QAAQ,CAAC,IAAI,IAAI,CAAC;CACnB;AA0BD,eAAO,MAAM,WAAW,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,YAgFnD,CAAC"}
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
2
+ import { defineComponent, useTheme } from '@textui/core';
3
+ import { Badge, Column, MarkdownView, Row } from '@textui/widgets';
4
+ const LOOK = {
5
+ pending: { tone: 'muted', glyph: 'bulletHollow' },
6
+ 'pending-confirmation': { tone: 'warning', glyph: 'bulletHalf' },
7
+ running: { tone: 'accent', glyph: 'bulletFilled' },
8
+ completed: { tone: 'success', glyph: 'check' },
9
+ failed: { tone: 'danger', glyph: 'cross' },
10
+ cancelled: { tone: 'muted', glyph: 'cross' },
11
+ };
12
+ /**
13
+ * The input, as one line.
14
+ *
15
+ * A tool's arguments are often JSON, and JSON arrives with newlines in it. Put
16
+ * straight on the row, a three-line object makes the row three lines tall and
17
+ * every other cell in it vertically centred - so the name floats beside the
18
+ * middle line of a brace-delimited block. The whole thing is on its own lines
19
+ * once the row is opened; this is the part that fits beside a name.
20
+ */
21
+ function oneLine(text) {
22
+ return text.replace(/\s+/g, ' ').trim();
23
+ }
24
+ export const ToolCallRow = defineComponent('ToolCallRow', (props) => {
25
+ const { call, expanded, active, onToggle, ...rest } = props;
26
+ const theme = useTheme();
27
+ const look = LOOK[call.status] ?? { tone: 'muted', glyph: 'bulletHollow' };
28
+ const glyph = theme.glyphs[look.glyph];
29
+ const chevron = expanded ? theme.glyphs.chevronDown : theme.glyphs.chevronRight;
30
+ const failed = call.status === 'failed' || (call.exitCode !== undefined && call.exitCode !== 0);
31
+ // What it is doing beats what it was asked, for as long as it is doing
32
+ // it: a subagent's row that says "look for the bug" for a minute is a
33
+ // row that says nothing, and the host has a line for what it is up to.
34
+ const summary = oneLine(call.progress ?? call.input ?? call.intention ?? '');
35
+ // Only when there is something under it. A chevron on a row that opens on
36
+ // to nothing is a promise the row cannot keep.
37
+ const opens = Boolean(call.intention ?? call.input ?? call.output ?? call.outcome
38
+ ?? (call.files && call.files.length > 0));
39
+ // On the selection the name, the summary and the chevron take `inverted`,
40
+ // the theme's own rule for that tone; the status glyph keeps its own, since
41
+ // a check that turned white would stop saying "completed".
42
+ return (_jsxs(Column, { ...rest, ...(active ? { bg: 'selected' } : {}), children: [_jsxs(Row, { gap: 1, ...(opens && onToggle ? { onClick: onToggle } : {}),
43
+ // The whole row lights up, not the glyph the pointer happens to be
44
+ // over: the row is the thing that opens.
45
+ style: { hover: { bg: 'hover' } }, children: [_jsx("text", { content: glyph, fg: look.tone }), _jsx("text", { content: call.name, bold: true, ...(active ? { fg: 'inverted' } : {}) }), _jsx("text", { content: summary, fg: active ? 'inverted' : 'muted', flex: 1, truncate: "middle" }), call.status === 'pending-confirmation' ? _jsx(Badge, { label: "asks", tone: "warning", icon: theme.glyphs.warning }) : null, failed ? _jsx(Badge, { label: `exit ${call.exitCode ?? 1}`, tone: "danger" }) : null, opens ? _jsx("text", { content: chevron, fg: active ? 'inverted' : 'subtle' }) : null] }), expanded ? (
46
+ // Indented to the row's own text, which starts one glyph and one gap
47
+ // in - so what opened out of a row lines up under it.
48
+ _jsxs(Column, { padding: [0, 0, 0, 2], gap: 0, children: [call.intention && call.intention !== call.name && call.intention !== call.input
49
+ ? _jsx(MarkdownView, { content: call.intention, quiet: true })
50
+ : null, call.input ? (
51
+ // On its own lines, wrapped as written. This is where the JSON
52
+ // goes: whole, and not sharing a row with the name.
53
+ _jsx(Column, { bg: "surfaceAlt", padding: [0, 1], children: call.input.split('\n').map((line, i) => (_jsx("text", { content: line, fg: "text", wrap: "word" }, i))) })) : null, call.output ? (_jsxs(Column, { children: [call.output.split('\n').slice(0, 12).map((line, i) => (_jsx("text", { content: line, fg: "muted", truncate: "end" }, i))), call.output.split('\n').length > 12 ? (_jsx("text", { content: `${theme.glyphs.ellipsis} ${call.output.split('\n').length - 12} more lines`, fg: "subtle" })) : null] })) : null, call.files && call.files.length > 0 ? (_jsx(Column, { children: call.files.map((file) => (_jsx("text", { content: `${theme.glyphs.chevronRight} ${file}`, fg: "info" }, file))) })) : null, call.outcome ? _jsx("text", { content: call.outcome, fg: "subtle" }) : null] })) : null] }));
54
+ });
@@ -0,0 +1,53 @@
1
+ import type { BoxProps, RenderOutput } from '@textui/core';
2
+ import type { Block } from './blocks.js';
3
+ /**
4
+ * The conversation, as blocks in a feed.
5
+ *
6
+ * There is no scrolling in this file. `Feed` owns the viewport, the cursor and
7
+ * the tail it follows, because none of that is about chat: a transcript, an
8
+ * activity stream and a list of search results with snippets are the same
9
+ * problem, which is "entries that are not one line tall". What is left here is
10
+ * the only part that *is* about chat - which block draws as what.
11
+ */
12
+ export interface ChatTranscriptProps extends BoxProps {
13
+ blocks: Block[];
14
+ expanded: Record<string, boolean>;
15
+ onToggle(id: string): void;
16
+ /** Which block the cursor is on. Held by the screen, like every other state. */
17
+ cursor?: number;
18
+ onCursor?(index: number): void;
19
+ /**
20
+ * What the find box is looking for.
21
+ *
22
+ * Passed down to be coloured where it appears, not to decide what is drawn:
23
+ * every block stays where it was and the ones holding the term light up, so
24
+ * a reader keeps the conversation around a hit instead of a filtered list
25
+ * of the lines that matched.
26
+ */
27
+ match?: string;
28
+ /**
29
+ * Keep the cursor in view rather than only when it moves.
30
+ *
31
+ * For the find box, which drives the cursor: its first hit is often the
32
+ * block the cursor is already on, and a feed that only scrolls on a change
33
+ * would leave that one off screen while the box counted it.
34
+ */
35
+ pinCursor?: boolean;
36
+ /**
37
+ * What this conversation is, as the first thing in it.
38
+ *
39
+ * Inside the scrolling region rather than pinned above it: a caption outside
40
+ * costs a row of the conversation on every screen for ever, so it has to
41
+ * earn each one - which is what forces it down to a line and then down to
42
+ * less than it was for. Here it costs nothing after the first screen.
43
+ *
44
+ * It is not a block. The cursor walks the conversation and there is nothing
45
+ * to do to a caption, so it sits ahead of the indices rather than in them.
46
+ */
47
+ head?: RenderOutput;
48
+ focusId?: string;
49
+ /** Prose and reasoning as markdown (the default), or as the characters that arrived. */
50
+ markdown?: boolean;
51
+ }
52
+ export declare const ChatTranscript: (props: ChatTranscriptProps) => RenderOutput;
53
+ //# sourceMappingURL=transcript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../src/transcript.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAIzC;;;;;;;;GAQG;AAEH,MAAM,WAAW,mBAAoB,SAAQ,QAAQ;IACnD,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,cAAc,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,YA2CzD,CAAC"}
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
2
+ import { defineComponent, useTheme } from '@textui/core';
3
+ import { Feed, Row } from '@textui/widgets';
4
+ import { ChatBubble, Gutter, ReasoningBlock, StreamingText, cursorBar } from './bubble.js';
5
+ import { ToolCallRow } from './toolcall.js';
6
+ export const ChatTranscript = defineComponent('ChatTranscript', (props) => {
7
+ const { blocks, expanded, onToggle, cursor, onCursor, head, markdown, match, pinCursor, focusId = 'chat.transcript', ...rest } = props;
8
+ // The caption is an entry the feed scrolls and the cursor does not visit,
9
+ // so every index the feed reports is one further along than the block it
10
+ // stands for. Converted here, once, rather than at each of the three
11
+ // places that would otherwise each have to remember.
12
+ const lead = head ? 1 : 0;
13
+ return (_jsxs(Feed, { focusId: focusId,
14
+ // Page up from the composer means the conversation above it. There is
15
+ // nothing else on this screen those keys could be for, and taking the
16
+ // keyboard off the field to use them is what a reader is avoiding.
17
+ pageKeys: "always", ...(cursor !== undefined ? { selectedIndex: cursor + lead } : {}), ...(pinCursor ? { pinSelection: true } : {}), ...(onCursor ? { onSelect: (index) => onCursor(Math.max(0, index - lead)) } : {}), onActivate: (index) => {
18
+ const block = blocks[index - lead];
19
+ if (block)
20
+ onToggle(block.id);
21
+ }, ...rest, children: [head ?? null, blocks.map((block) => (_jsx(BlockView, { block: block, expanded: expanded[block.id] ?? false, active: cursor !== undefined && blocks[cursor]?.id === block.id, onToggle: () => onToggle(block.id), ...(markdown !== undefined ? { markdown } : {}), ...(match ? { match } : {}) }, block.id)))] }));
22
+ });
23
+ const BlockView = defineComponent('ChatBlockView', ({ block, expanded, active, onToggle, markdown, match }) => {
24
+ const asMarkdown = markdown !== undefined ? { markdown } : {};
25
+ // Spread rather than passed, so a block with no search over it carries no
26
+ // extra prop and its text node is compared unchanged.
27
+ const hit = match ? { match } : {};
28
+ const theme = useTheme();
29
+ // Every block has a one-cell left column the cursor is drawn in. The blocks
30
+ // that are something said keep the rule they draw there; a block whose
31
+ // first row already carries a glyph in that column - the header's bullet,
32
+ // the user line's chevron - has that glyph as its gutter cell, and the bar
33
+ // takes its place while the cursor is on it; the rest lead with a blank
34
+ // gutter, so their text starts where the prose does.
35
+ const mark = active ? { active: true } : {};
36
+ switch (block.kind) {
37
+ case 'said':
38
+ // The blank row is the turn boundary. A uniform gap between every block
39
+ // would space a paragraph from the sentence it belongs to just as much
40
+ // as it spaces one speaker from the next.
41
+ return (_jsx(ChatBubble, { speaker: "user", padding: [1, 0, 0, 0], ...mark, children: _jsx("text", { content: block.text, wrap: "word", ...hit }) }));
42
+ case 'header':
43
+ return (_jsxs(Row, { gap: 1, padding: [1, 0, 0, 0], children: [_jsx("text", { content: active ? cursorBar(theme) : theme.glyphs.bulletFilled, fg: active || block.state === 'running' ? 'accent' : 'muted' }), _jsx("text", { content: block.model ?? 'agent', bold: true, fg: "accent" }), block.settings ? _jsx("text", { content: block.settings, fg: "subtle" }) : null, _jsx("text", { content: block.meta, fg: "subtle", flex: 1 }), block.state === 'cancelled' ? _jsx("text", { content: "stopped", fg: "warning" }) : null, block.state === 'failed' ? _jsx("text", { content: "failed", fg: "danger" }) : null] }));
44
+ case 'prose':
45
+ return (_jsxs(Row, { gap: 1, children: [_jsx(Gutter, { ...mark }), _jsx(StreamingText, { content: block.content, streaming: block.streaming, flex: 1, ...asMarkdown, ...hit })] }));
46
+ case 'reasoning':
47
+ return (_jsxs(Row, { gap: 1, children: [_jsx(Gutter, { ...mark }), _jsx(ReasoningBlock, { content: block.content, expanded: expanded, streaming: block.streaming, onToggle: onToggle, flex: 1, ...mark, ...asMarkdown, ...hit })] }));
48
+ case 'notice':
49
+ return (_jsxs(Row, { gap: 1, children: [_jsx(Gutter, { blank: true, ...mark }), _jsx("text", { content: theme.glyphs.info, fg: "info" }), _jsx("text", { content: block.content, fg: "muted", wrap: "word", flex: 1, ...hit })] }));
50
+ // Not a notice. A notice is the harness saying something in passing, and
51
+ // this is the turn stopping - so it takes the danger tone and says whether
52
+ // there is anything to carry on from.
53
+ case 'failure':
54
+ return (_jsxs(Row, { gap: 1, children: [_jsx(Gutter, { blank: true, ...mark }), _jsx("text", { content: theme.glyphs.cross, fg: "danger" }), _jsx("text", { content: block.content, fg: "danger", wrap: "word", flex: 1, ...hit }), block.resumable ? _jsx("text", { content: "resumable", fg: "subtle" }) : null] }));
55
+ case 'tool':
56
+ // No rule. A tool call is something the agent *did*, not something it
57
+ // said, so its status glyph sits where the header's bullet is - rather
58
+ // than inside the rule as though it were a paragraph of the answer.
59
+ return (_jsxs(Row, { gap: 1, children: [_jsx(Gutter, { blank: true, ...mark }), _jsx(ToolCallRow, { call: block.call, expanded: expanded, active: active, onToggle: onToggle, flex: 1 })] }));
60
+ case 'queued':
61
+ // Not sent. It reads as a message unless it says so, and "I typed that
62
+ // and nothing happened" is the complaint that follows.
63
+ return (_jsxs(Row, { gap: 1, children: [_jsx(Gutter, { blank: true, ...mark }), _jsx("text", { content: theme.glyphs.chevronRight, fg: active ? 'accent' : 'subtle' }), _jsx("text", { content: block.text, fg: "subtle", italic: true, wrap: "word", flex: 1, ...hit }), _jsx("text", { content: active ? 'enter drops it' : 'queued', fg: "warning" })] }));
64
+ default:
65
+ return null;
66
+ }
67
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * What the chat components are told, in the components' own words.
3
+ *
4
+ * These are view shapes, not a protocol. A client that speaks AHP, or
5
+ * anything else, maps its own records onto them and the components never
6
+ * learn where a session or a tool call came from. The field names follow the
7
+ * Agent Host Protocol's where one exists, so that mapping is a pick rather
8
+ * than a rename - but nothing here is imported from it, and nothing here
9
+ * says how a session is fetched.
10
+ */
11
+ export type ChatToolCallStatus = 'pending' | 'pending-confirmation' | 'running' | 'completed' | 'failed' | 'cancelled';
12
+ /** One tool call, flat: the fields that are not there yet are absent. */
13
+ export interface ChatToolCall {
14
+ id: string;
15
+ /** What the row calls it. */
16
+ name: string;
17
+ /**
18
+ * The tool's own id, where it differs from the display name.
19
+ *
20
+ * Hosts give many tools one display name - every subagent is "Explore" or
21
+ * "Plan" and the tool under all of them is `Task` - and a person searching
22
+ * the transcript for the one or the other should find the row either way.
23
+ */
24
+ toolName?: string;
25
+ status: ChatToolCallStatus;
26
+ /** The command. The only thing separating twenty identical rows. */
27
+ input?: string;
28
+ /** What it meant to do. Markdown. */
29
+ intention?: string;
30
+ /**
31
+ * What it is doing right now, while it runs.
32
+ *
33
+ * A line drawn on a running row and dropped when the row ends: a
34
+ * subagent's own summary of how far it has got, the last tool it reached
35
+ * for. Read only while `running`; a host that leaves it on a finished call
36
+ * is still describing a state the call is no longer in.
37
+ */
38
+ progress?: string;
39
+ /** What it did, past tense. */
40
+ outcome?: string;
41
+ /** What came back. */
42
+ output?: string;
43
+ exitCode?: number;
44
+ files?: string[];
45
+ /** Set while `pending-confirmation`. */
46
+ confirmationTitle?: string;
47
+ options?: {
48
+ id: string;
49
+ label: string;
50
+ }[];
51
+ }
52
+ export type ChatActivity = 'input' | 'running' | 'error' | 'idle';
53
+ /**
54
+ * A session's state, already decoded.
55
+ *
56
+ * The word, the colour and the glyph travel together because none of them is
57
+ * allowed to be the only carrier: a piped log keeps the word, a 16-colour
58
+ * terminal keeps the glyph, and a reader who cannot see the colour keeps both.
59
+ */
60
+ export interface ChatSessionStatus {
61
+ activity: ChatActivity;
62
+ archived: boolean;
63
+ read: boolean;
64
+ label: string;
65
+ tone: 'warning' | 'accent' | 'danger' | 'muted';
66
+ glyph: 'bulletHalf' | 'bulletFilled' | 'cross' | 'bulletHollow';
67
+ }
68
+ /** One row of the catalogue, and the head over a conversation. */
69
+ export interface ChatSession {
70
+ id: string;
71
+ title: string;
72
+ /** Which harness runs it. */
73
+ provider: string;
74
+ status: ChatSessionStatus;
75
+ createdAt: string;
76
+ modifiedAt: string;
77
+ workingDirectories: string[];
78
+ /** The project as the host names it, when it does. */
79
+ project?: string;
80
+ branch?: string;
81
+ /**
82
+ * The pull request the branch became, as the row says it: `#412 merged`.
83
+ *
84
+ * Already a label. Which host key holds the number and which the state is
85
+ * the client's to know; the row only has one line to say it on.
86
+ */
87
+ pullRequest?: string;
88
+ /** What the host says it is doing, in its own words. */
89
+ activity?: string;
90
+ /** Why it is here when nobody started it, as a phrase: "by an automation". */
91
+ origin?: string;
92
+ changes?: {
93
+ files?: number;
94
+ additions?: number;
95
+ deletions?: number;
96
+ };
97
+ }
98
+ export type ChatQuestionKind = 'text' | 'number' | 'integer' | 'boolean' | 'single-select' | 'multi-select';
99
+ export interface ChatQuestion {
100
+ id: string;
101
+ kind: ChatQuestionKind;
102
+ message: string;
103
+ required?: boolean;
104
+ options?: {
105
+ id: string;
106
+ label: string;
107
+ }[];
108
+ /** Answering in words *instead of* choosing, not as a choice. */
109
+ allowFreeformInput?: boolean;
110
+ }
111
+ export type ChatAnswer = {
112
+ kind: 'text';
113
+ value: string;
114
+ } | {
115
+ kind: 'number';
116
+ value: number;
117
+ } | {
118
+ kind: 'boolean';
119
+ value: boolean;
120
+ } | {
121
+ kind: 'selected';
122
+ value: string;
123
+ } | {
124
+ kind: 'selected-many';
125
+ value: string[];
126
+ };
127
+ /** A yes or a no about a command, with named options where there are any. */
128
+ export interface ChatToolConfirmation {
129
+ kind: 'toolConfirmation';
130
+ id: string;
131
+ call: ChatToolCall;
132
+ }
133
+ /** A request in prose, with the questions under it. */
134
+ export interface ChatInputRequest {
135
+ kind: 'chatInput';
136
+ id: string;
137
+ message: string;
138
+ questions: ChatQuestion[];
139
+ }
140
+ export type ChatPendingInput = ChatToolConfirmation | ChatInputRequest;
141
+ /** One entry of the path menu under the composer. */
142
+ export interface ChatCompletion {
143
+ /** What to put in the draft. */
144
+ insertText: string;
145
+ /** Where the replaced fragment starts, as an offset into the draft. */
146
+ rangeStart: number;
147
+ /** Where it ends. */
148
+ rangeEnd: number;
149
+ /** What a person reads in the menu. */
150
+ label: string;
151
+ /** One line under it, when there is something worth reading. */
152
+ description?: string;
153
+ }
154
+ /** One entry of the slash menu. */
155
+ export interface ChatCommand {
156
+ id: string;
157
+ kind: 'client' | 'session';
158
+ title: string;
159
+ description?: string;
160
+ /** Where a session command came from: the plugin or directory. */
161
+ from?: string;
162
+ /**
163
+ * What goes after the name, written the way it would be typed.
164
+ *
165
+ * `/autocompact <tokens>` says more about the command than a sentence
166
+ * describing it, and it is the one thing a menu cannot show in a row: the
167
+ * row is the name.
168
+ */
169
+ hint?: string;
170
+ }
171
+ /** The row under the composer: gone to the host, or never going. */
172
+ export interface ChatSendStatus {
173
+ state: 'sending' | 'failed';
174
+ text: string;
175
+ }
176
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * What the chat components are told, in the components' own words.
3
+ *
4
+ * These are view shapes, not a protocol. A client that speaks AHP, or
5
+ * anything else, maps its own records onto them and the components never
6
+ * learn where a session or a tool call came from. The field names follow the
7
+ * Agent Host Protocol's where one exists, so that mapping is a pick rather
8
+ * than a rename - but nothing here is imported from it, and nothing here
9
+ * says how a session is fetched.
10
+ */
11
+ export {};
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@textui/chat",
3
+ "version": "0.6.0",
4
+ "description": "The TextUI chat components - transcript, composer, tool calls, questions, sessions and diffs",
5
+ "keywords": [
6
+ "terminal",
7
+ "tui",
8
+ "chat",
9
+ "agent",
10
+ "components",
11
+ "jsx",
12
+ "ui",
13
+ "text-ui"
14
+ ],
15
+ "homepage": "https://softov.github.io/textui/",
16
+ "bugs": {
17
+ "url": "https://github.com/softov/textui/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/softov/textui.git",
22
+ "directory": "packages/chat"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Softov <softov@brbyte.com>",
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "src",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "import": "./dist/index.js"
41
+ }
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
46
+ "test": "vitest run"
47
+ },
48
+ "dependencies": {
49
+ "@textui/core": "workspace:^",
50
+ "@textui/widgets": "workspace:^"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "devDependencies": {
56
+ "@textui/testing": "workspace:*"
57
+ }
58
+ }