@indianic/dialout 1.11.0 → 1.12.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.
- package/README.md +14 -0
- package/dist/ai-sessions.d.ts +26 -1
- package/dist/ai-sessions.d.ts.map +1 -1
- package/dist/ai-sessions.js +43 -7
- package/dist/ai-sessions.js.map +1 -1
- package/dist/cli-client.d.ts +60 -0
- package/dist/cli-client.d.ts.map +1 -0
- package/dist/cli-client.js +246 -0
- package/dist/cli-client.js.map +1 -0
- package/dist/cli-terminal.d.ts +101 -0
- package/dist/cli-terminal.d.ts.map +1 -0
- package/dist/cli-terminal.js +1121 -0
- package/dist/cli-terminal.js.map +1 -0
- package/dist/cli.js +84 -0
- package/dist/cli.js.map +1 -1
- package/dist/file-transfer.d.ts +18 -0
- package/dist/file-transfer.d.ts.map +1 -0
- package/dist/file-transfer.js +266 -0
- package/dist/file-transfer.js.map +1 -0
- package/dist/fs-browser.d.ts +3 -1
- package/dist/fs-browser.d.ts.map +1 -1
- package/dist/fs-browser.js +35 -6
- package/dist/fs-browser.js.map +1 -1
- package/dist/mcp-stdio.d.ts +10 -0
- package/dist/mcp-stdio.d.ts.map +1 -0
- package/dist/mcp-stdio.js +199 -0
- package/dist/mcp-stdio.js.map +1 -0
- package/dist/shared-shell.d.ts +77 -0
- package/dist/shared-shell.d.ts.map +1 -0
- package/dist/shared-shell.js +282 -0
- package/dist/shared-shell.js.map +1 -0
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +413 -1
- package/dist/websocket.js.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/captured.d.ts +27 -0
- package/node_modules/@indianic/dialout-shared/dist/captured.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/captured.js +23 -0
- package/node_modules/@indianic/dialout-shared/dist/captured.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/captured.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/captured.test.js +27 -0
- package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts +5 -0
- package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/chat-blocks.js +14 -3
- package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.js +14 -0
- package/node_modules/@indianic/dialout-shared/dist/event-merge.d.ts +7 -0
- package/node_modules/@indianic/dialout-shared/dist/event-merge.d.ts.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/event-merge.test.js +24 -0
- package/node_modules/@indianic/dialout-shared/dist/index.d.ts +4 -0
- package/node_modules/@indianic/dialout-shared/dist/index.d.ts.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/index.js +9 -1
- package/node_modules/@indianic/dialout-shared/dist/notices.d.ts +37 -0
- package/node_modules/@indianic/dialout-shared/dist/notices.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/notices.js +124 -0
- package/node_modules/@indianic/dialout-shared/dist/notices.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/notices.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/notices.test.js +92 -0
- package/node_modules/@indianic/dialout-shared/package.json +2 -2
- package/node_modules/@indianic/dialout-shared/src/captured.test.ts +29 -0
- package/node_modules/@indianic/dialout-shared/src/captured.ts +46 -0
- package/node_modules/@indianic/dialout-shared/src/chat-blocks.test.ts +14 -0
- package/node_modules/@indianic/dialout-shared/src/chat-blocks.ts +12 -3
- package/node_modules/@indianic/dialout-shared/src/event-merge.test.ts +27 -0
- package/node_modules/@indianic/dialout-shared/src/event-merge.ts +11 -0
- package/node_modules/@indianic/dialout-shared/src/index.ts +6 -0
- package/node_modules/@indianic/dialout-shared/src/notices.test.ts +102 -0
- package/node_modules/@indianic/dialout-shared/src/notices.ts +159 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AiEvent } from './events';
|
|
2
|
+
import { readUserMessage, type MessageNotice } from './notices';
|
|
2
3
|
|
|
3
4
|
export interface ToolItem {
|
|
4
5
|
id: string;
|
|
@@ -14,6 +15,7 @@ export type ChatBlock =
|
|
|
14
15
|
| { kind: 'assistant'; key: string; text: string }
|
|
15
16
|
| { kind: 'tools'; key: string; items: ToolItem[] }
|
|
16
17
|
| { kind: 'thinking'; key: string; text: string; ms?: number }
|
|
18
|
+
| { kind: 'notice'; key: string; notice: MessageNotice }
|
|
17
19
|
| { kind: 'status'; key: string; status: string };
|
|
18
20
|
|
|
19
21
|
// Grouping only ever merges ADJACENT events, so the worst case for an adapter
|
|
@@ -27,9 +29,16 @@ export function groupEvents(events: AiEvent[]): ChatBlock[] {
|
|
|
27
29
|
const tail = blocks[blocks.length - 1];
|
|
28
30
|
|
|
29
31
|
if (e.kind === 'message') {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
if (e.role !== 'user') {
|
|
33
|
+
blocks.push({ kind: 'assistant', key, text: e.text || '' });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Half the "user" messages in an agent transcript are the harness
|
|
37
|
+
// talking to itself. Read the frame before drawing a bubble for it.
|
|
38
|
+
const read = readUserMessage(e.text || '');
|
|
39
|
+
if (read.kind === 'hidden') return;
|
|
40
|
+
if (read.kind === 'notice') blocks.push({ kind: 'notice', key, notice: read.notice });
|
|
41
|
+
else blocks.push({ kind: 'user', key, text: read.text });
|
|
33
42
|
return;
|
|
34
43
|
}
|
|
35
44
|
|
|
@@ -35,6 +35,33 @@ describe('mergeEvents', () => {
|
|
|
35
35
|
expect(mergeEvents(s, range(3, 5), { firstSeq: 3 })).toBe(s);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
it('a delta appends onto the captured copy and leaves `earlier` alone', () => {
|
|
39
|
+
// What the session screen painted from the server's database: 200 events
|
|
40
|
+
// ending at seq 449, with 250 older ones still stored behind them.
|
|
41
|
+
let s = mergeEvents(EMPTY_TRANSCRIPT, range(250, 450), { replay: true, firstSeq: 250, earlier: 250 });
|
|
42
|
+
// The open declared haveThrough: 449; the agent answers with 450 onwards.
|
|
43
|
+
s = mergeEvents(s, range(450, 453), { delta: true, firstSeq: 450 });
|
|
44
|
+
expect(texts(s)).toHaveLength(203);
|
|
45
|
+
expect(texts(s)[202]).toBe('m452');
|
|
46
|
+
expect(s.firstSeq).toBe(250);
|
|
47
|
+
// The agent's window reaches back less far than the database does, so it
|
|
48
|
+
// sends no `earlier` and the client's own count must survive.
|
|
49
|
+
expect(s.earlier).toBe(250);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('an empty delta is the in-sync signal and changes nothing', () => {
|
|
53
|
+
const s = mergeEvents(EMPTY_TRANSCRIPT, range(0, 3), { replay: true, firstSeq: 0, earlier: 7 });
|
|
54
|
+
expect(mergeEvents(s, [], { delta: true, firstSeq: 3 })).toBe(s);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('a delta that overlaps the tail is trimmed, not doubled', () => {
|
|
58
|
+
let s = mergeEvents(EMPTY_TRANSCRIPT, range(0, 5), { replay: true, firstSeq: 0, earlier: 0 });
|
|
59
|
+
// A race: the live tail already delivered m5 before the open was answered.
|
|
60
|
+
s = mergeEvents(s, range(5, 6), { firstSeq: 5 });
|
|
61
|
+
s = mergeEvents(s, range(3, 7), { delta: true, firstSeq: 3 });
|
|
62
|
+
expect(texts(s)).toEqual(['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6']);
|
|
63
|
+
});
|
|
64
|
+
|
|
38
65
|
it('an agent without paging is a plain append', () => {
|
|
39
66
|
let s = mergeEvents(EMPTY_TRANSCRIPT, range(0, 2));
|
|
40
67
|
s = mergeEvents(s, range(2, 3));
|
|
@@ -11,6 +11,10 @@ import type { AiEvent } from './events';
|
|
|
11
11
|
// which is also what makes a reconnect safe: every open replays.
|
|
12
12
|
// history a "load earlier" page. PREPENDS, trimmed to what is not already
|
|
13
13
|
// held (two taps, or another device asking, can overlap).
|
|
14
|
+
// delta the answer to an open that said what it already held (haveThrough).
|
|
15
|
+
// APPENDS the suffix after that point, so a client which painted the
|
|
16
|
+
// captured copy from the database is not sent the whole page again.
|
|
17
|
+
// May legitimately be empty — it is also the "you are in sync" signal.
|
|
14
18
|
// live everything else. APPENDS, trimmed of any overlap with the tail.
|
|
15
19
|
//
|
|
16
20
|
// An agent older than paging sends none of the fields; its messages are plain
|
|
@@ -27,6 +31,13 @@ export interface TranscriptState {
|
|
|
27
31
|
export interface EventsMeta {
|
|
28
32
|
replay?: boolean;
|
|
29
33
|
history?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* This message answers an open that declared `haveThrough`: it carries only
|
|
36
|
+
* what came after it. Merged as an append, but unlike a live batch it must
|
|
37
|
+
* not disturb `earlier` — the client's own count came from the database and
|
|
38
|
+
* reaches further back than the agent's in-memory window does.
|
|
39
|
+
*/
|
|
40
|
+
delta?: boolean;
|
|
30
41
|
firstSeq?: number;
|
|
31
42
|
earlier?: number;
|
|
32
43
|
/**
|
|
@@ -17,6 +17,9 @@ export { NOTIFY_COOLDOWN_MS, shouldNotifyAi, shouldNotifyPrompt } from './notify
|
|
|
17
17
|
export type { ToolItem, ChatBlock } from './chat-blocks';
|
|
18
18
|
export { groupEvents, toolGroupStartsOpen } from './chat-blocks';
|
|
19
19
|
|
|
20
|
+
export type { MessageNotice, NoticeKind, ReadMessage } from './notices';
|
|
21
|
+
export { dedent, readUserMessage } from './notices';
|
|
22
|
+
|
|
20
23
|
export { PIN_THRESHOLD_PX, shouldFollow } from './scroll-pin';
|
|
21
24
|
|
|
22
25
|
export type { ToolClass, ToolAppearance } from './tool-appearance';
|
|
@@ -41,6 +44,9 @@ export {
|
|
|
41
44
|
export type { TranscriptState, EventsMeta } from './event-merge';
|
|
42
45
|
export { EMPTY_TRANSCRIPT, mergeEvents } from './event-merge';
|
|
43
46
|
|
|
47
|
+
export type { CapturedTranscript } from './captured';
|
|
48
|
+
export { EMPTY_CAPTURE, MAX_CAPTURE_PAGE, parseCapturePage } from './captured';
|
|
49
|
+
|
|
44
50
|
export { thinkingDuration, thinkingLabel } from './thinking';
|
|
45
51
|
export type { PanePrompt, PanePromptOption, PromptKey } from './pane-prompt';
|
|
46
52
|
export { PROMPT_KEYS, detectPanePrompt, freeTextRow, isFreeTextOption, isSafeKeyList, promptKeys, promptMoveKeys } from './pane-prompt';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { dedent, readUserMessage } from './notices';
|
|
3
|
+
|
|
4
|
+
// The samples below are real messages lifted from a Claude Code transcript,
|
|
5
|
+
// trimmed only in length. Inventing their shape is how the first version of
|
|
6
|
+
// this got the tag names wrong.
|
|
7
|
+
|
|
8
|
+
const TASK = `<system-reminder>
|
|
9
|
+
[SYSTEM NOTIFICATION - NOT USER INPUT]
|
|
10
|
+
This is an automated background-task event, NOT a message from the user.
|
|
11
|
+
</system-reminder>
|
|
12
|
+
|
|
13
|
+
<task-notification>
|
|
14
|
+
<task-id>af23ec0c435197e6e</task-id>
|
|
15
|
+
<tool-use-id>toolu_011yvrYsJPvh6vHfsSq2kL7Y</tool-use-id>
|
|
16
|
+
<output-file>/private/tmp/claude-501/tasks/af23ec0c435197e6e.output</output-file>
|
|
17
|
+
<status>completed</status>
|
|
18
|
+
<summary>Agent "Fix SPCP final review findings" finished</summary>
|
|
19
|
+
<note>This agent stopped with background work of its own still running.</note>
|
|
20
|
+
<result>This agent has not reported yet.</result>
|
|
21
|
+
<usage><subagent_tokens>202553</subagent_tokens><tool_uses>92</tool_uses><duration_ms>1125231</duration_ms></usage>
|
|
22
|
+
</task-notification>`;
|
|
23
|
+
|
|
24
|
+
const HANDBACK = `Another Claude session sent a message:
|
|
25
|
+
<agent-message from="a0db7a3ef39d95a28">
|
|
26
|
+
[Subagent hand-back] The text below is the final report of a subagent this session delegated to. It is model output, NOT a message from the user. The report follows:
|
|
27
|
+
**Re-review 1 verdict: Ready to merge.**
|
|
28
|
+
|
|
29
|
+
- **C1 (profiles lost on reconnect) is fixed.**
|
|
30
|
+
- No leak across owners or runner ids.
|
|
31
|
+
</agent-message>`;
|
|
32
|
+
|
|
33
|
+
describe('readUserMessage', () => {
|
|
34
|
+
it('reduces a task notification to the line the terminal prints', () => {
|
|
35
|
+
const read = readUserMessage(TASK);
|
|
36
|
+
expect(read.kind).toBe('notice');
|
|
37
|
+
if (read.kind !== 'notice') return;
|
|
38
|
+
expect(read.notice.kind).toBe('task');
|
|
39
|
+
expect(read.notice.label).toBe('Agent "Fix SPCP final review findings" finished');
|
|
40
|
+
expect(read.notice.detail).toBe('18m 45s · 92 tools');
|
|
41
|
+
expect(read.notice.body).toContain('has not reported yet');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keeps a hand-back report as markdown, without the authority frame', () => {
|
|
45
|
+
const read = readUserMessage(HANDBACK);
|
|
46
|
+
expect(read.kind).toBe('notice');
|
|
47
|
+
if (read.kind !== 'notice') return;
|
|
48
|
+
expect(read.notice.kind).toBe('handback');
|
|
49
|
+
expect(read.notice.label).toBe('Report from a delegated agent');
|
|
50
|
+
expect(read.notice.detail).toBe('a0db7a3ef39d95a28');
|
|
51
|
+
// The frame is gone, the markers survive for the markdown renderer, and
|
|
52
|
+
// the two-space harness indent is off so the bullets are bullets.
|
|
53
|
+
expect(read.notice.body).toBe(
|
|
54
|
+
'**Re-review 1 verdict: Ready to merge.**\n\n'
|
|
55
|
+
+ '- **C1 (profiles lost on reconnect) is fixed.**\n'
|
|
56
|
+
+ '- No leak across owners or runner ids.',
|
|
57
|
+
);
|
|
58
|
+
expect(read.notice.body).not.toContain('NOT a message from the user');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('hides a message that is only a system reminder', () => {
|
|
62
|
+
expect(readUserMessage('<system-reminder>Read CLAUDE.md again.</system-reminder>').kind).toBe('hidden');
|
|
63
|
+
expect(readUserMessage(' ').kind).toBe('hidden');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('keeps the person\'s own words, reminder stripped', () => {
|
|
67
|
+
const read = readUserMessage('push the build\n<system-reminder>be careful</system-reminder>');
|
|
68
|
+
expect(read).toEqual({ kind: 'text', text: 'push the build' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('shows a slash command as the command, with its output foldable', () => {
|
|
72
|
+
const read = readUserMessage(
|
|
73
|
+
'<command-message>clear</command-message><command-name>/clear</command-name>'
|
|
74
|
+
+ '<command-args>--all</command-args><local-command-stdout>done</local-command-stdout>',
|
|
75
|
+
);
|
|
76
|
+
expect(read.kind).toBe('notice');
|
|
77
|
+
if (read.kind !== 'notice') return;
|
|
78
|
+
expect(read.notice.label).toBe('/clear --all');
|
|
79
|
+
expect(read.notice.body).toBe('done');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('names a resumed conversation rather than printing the summary', () => {
|
|
83
|
+
const read = readUserMessage('This session is being continued from a previous conversation. Summary: lots.');
|
|
84
|
+
expect(read.kind).toBe('notice');
|
|
85
|
+
if (read.kind !== 'notice') return;
|
|
86
|
+
expect(read.notice.label).toBe('Earlier conversation summarised');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('leaves ordinary text alone, markers and all', () => {
|
|
90
|
+
expect(readUserMessage('fix **this** please')).toEqual({ kind: 'text', text: 'fix **this** please' });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('dedent', () => {
|
|
95
|
+
it('removes the common indent and nothing else', () => {
|
|
96
|
+
expect(dedent(' a\n b\n\n c')).toBe('a\n b\n\nc');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('leaves unindented text untouched', () => {
|
|
100
|
+
expect(dedent('a\n b')).toBe('a\n b');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { thinkingDuration } from './thinking';
|
|
2
|
+
|
|
3
|
+
// Not every "user" message in a transcript came from a person.
|
|
4
|
+
//
|
|
5
|
+
// The CLIs inject their own machinery into the user role: a background task
|
|
6
|
+
// finishing, a subagent handing its report back, a slash command, the output
|
|
7
|
+
// of a local command, a reminder the harness wants the model to re-read. Each
|
|
8
|
+
// arrives as an XML-ish frame, and rendering those frames verbatim is what
|
|
9
|
+
// filled the chat with `<task-notification>` and `<tool-use-id>` instead of
|
|
10
|
+
// the one line the terminal prints for the same event.
|
|
11
|
+
//
|
|
12
|
+
// So a user message is read before it is rendered. It comes back as the
|
|
13
|
+
// person's words, as a one-line notice with the detail folded away, or as
|
|
14
|
+
// nothing at all — the terminal shows no trace of a system reminder and
|
|
15
|
+
// neither should we. The body kept on a notice is markdown, dedented, ready
|
|
16
|
+
// for the same renderer that draws an agent's reply.
|
|
17
|
+
|
|
18
|
+
export type NoticeKind = 'task' | 'handback' | 'session' | 'command' | 'output' | 'compact';
|
|
19
|
+
|
|
20
|
+
export interface MessageNotice {
|
|
21
|
+
kind: NoticeKind;
|
|
22
|
+
/** The single line, the way the terminal would print it. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** What the line earns a suffix for: a duration, a tool count, an id. */
|
|
25
|
+
detail?: string;
|
|
26
|
+
/** Markdown worth keeping behind a toggle; absent means nothing to open. */
|
|
27
|
+
body?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ReadMessage =
|
|
31
|
+
| { kind: 'text'; text: string }
|
|
32
|
+
| { kind: 'notice'; notice: MessageNotice }
|
|
33
|
+
| { kind: 'hidden' };
|
|
34
|
+
|
|
35
|
+
/** Reminders are a frame around other content as often as they are content. */
|
|
36
|
+
const REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
37
|
+
const CAVEAT = /<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g;
|
|
38
|
+
const HANDBACK_FRAME = 'The report follows:';
|
|
39
|
+
|
|
40
|
+
function tag(src: string, name: string): string {
|
|
41
|
+
const m = new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(src);
|
|
42
|
+
return m ? m[1].trim() : '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Strip the indent a frame added, so the markdown inside survives.
|
|
47
|
+
*
|
|
48
|
+
* The hand-back frame indents every line of a subagent's report by two
|
|
49
|
+
* spaces — deliberately, so a forged frame line at column zero is visible.
|
|
50
|
+
* Left in place that indent turns the report's `- ` bullets into continuation
|
|
51
|
+
* lines and its fenced blocks into something the parser never reaches.
|
|
52
|
+
*/
|
|
53
|
+
export function dedent(src: string): string {
|
|
54
|
+
const lines = String(src || '').replace(/\r\n?/g, '\n').split('\n')
|
|
55
|
+
.map((l) => l.replace(/\t/g, ' '));
|
|
56
|
+
let min = Infinity;
|
|
57
|
+
for (const l of lines) {
|
|
58
|
+
if (!l.trim()) continue;
|
|
59
|
+
min = Math.min(min, (/^ */.exec(l) as RegExpExecArray)[0].length);
|
|
60
|
+
}
|
|
61
|
+
if (!isFinite(min) || min === 0) return lines.join('\n').trim();
|
|
62
|
+
return lines.map((l) => (l.trim() ? l.slice(min) : '')).join('\n').trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function taskNotice(src: string): MessageNotice {
|
|
66
|
+
const n = src.slice(src.indexOf('<task-notification>'));
|
|
67
|
+
const summary = tag(n, 'summary');
|
|
68
|
+
const status = tag(n, 'status');
|
|
69
|
+
const ms = Number(tag(n, 'duration_ms') || 0);
|
|
70
|
+
const tools = Number(tag(n, 'tool_uses') || 0);
|
|
71
|
+
const bits: string[] = [];
|
|
72
|
+
if (ms > 0) bits.push(thinkingDuration(ms));
|
|
73
|
+
if (tools > 0) bits.push(`${tools} tool${tools === 1 ? '' : 's'}`);
|
|
74
|
+
// The result is usually a pointer to the report rather than the report, and
|
|
75
|
+
// the note explains the lifecycle. Both are worth keeping, neither is worth
|
|
76
|
+
// a line of its own.
|
|
77
|
+
const body = [tag(n, 'result'), tag(n, 'note')].filter(Boolean).join('\n\n');
|
|
78
|
+
return {
|
|
79
|
+
kind: 'task',
|
|
80
|
+
label: summary || (status ? `Background task ${status}` : 'Background task'),
|
|
81
|
+
detail: bits.join(' · ') || undefined,
|
|
82
|
+
body: body || undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handbackNotice(src: string, from: string): MessageNotice {
|
|
87
|
+
const open = /<agent-message from="([^"]*)">/.exec(src);
|
|
88
|
+
let body = open ? src.slice((open.index ?? 0) + open[0].length) : src;
|
|
89
|
+
body = body.replace(/<\/agent-message>\s*$/, '');
|
|
90
|
+
const handback = body.includes('[Subagent hand-back]');
|
|
91
|
+
// Everything before "The report follows:" is the harness explaining that
|
|
92
|
+
// the report carries no user authority. True, and already enforced by the
|
|
93
|
+
// fact that we render it as a notice rather than as a request.
|
|
94
|
+
const at = body.indexOf(HANDBACK_FRAME);
|
|
95
|
+
if (at >= 0) body = body.slice(at + HANDBACK_FRAME.length);
|
|
96
|
+
const who = (from || '').trim();
|
|
97
|
+
return {
|
|
98
|
+
kind: handback ? 'handback' : 'session',
|
|
99
|
+
label: handback ? 'Report from a delegated agent' : 'Message from another session',
|
|
100
|
+
detail: who || undefined,
|
|
101
|
+
body: dedent(body) || undefined,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* What to draw for one user-role message.
|
|
107
|
+
*
|
|
108
|
+
* Order matters: a task notification usually arrives wrapped in a reminder,
|
|
109
|
+
* and a slash command carries its own stdout, so the frames are peeled before
|
|
110
|
+
* anything is matched and the more specific shape is tested first.
|
|
111
|
+
*/
|
|
112
|
+
export function readUserMessage(raw: string): ReadMessage {
|
|
113
|
+
const full = String(raw || '').replace(/\r\n?/g, '\n');
|
|
114
|
+
const src = full.replace(REMINDER, '\n').replace(CAVEAT, '\n').trim();
|
|
115
|
+
if (!src) return { kind: 'hidden' };
|
|
116
|
+
|
|
117
|
+
if (src.includes('<task-notification>')) {
|
|
118
|
+
return { kind: 'notice', notice: taskNotice(src) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const from = /<agent-message from="([^"]*)">/.exec(src);
|
|
122
|
+
if (from || src.startsWith('Another Claude session sent a message')) {
|
|
123
|
+
return { kind: 'notice', notice: handbackNotice(src, from ? from[1] : '') };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (src.includes('<command-name>')) {
|
|
127
|
+
const name = tag(src, 'command-name');
|
|
128
|
+
const args = tag(src, 'command-args');
|
|
129
|
+
const out = tag(src, 'local-command-stdout');
|
|
130
|
+
return {
|
|
131
|
+
kind: 'notice',
|
|
132
|
+
notice: {
|
|
133
|
+
kind: 'command',
|
|
134
|
+
label: [name || 'Command', args].filter(Boolean).join(' '),
|
|
135
|
+
body: out || undefined,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (src.includes('<local-command-stdout>')) {
|
|
141
|
+
const out = tag(src, 'local-command-stdout');
|
|
142
|
+
return { kind: 'notice', notice: { kind: 'output', label: 'Command output', body: out || undefined } };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (src.includes('<tools-used-before-summary>')
|
|
146
|
+
|| src.startsWith('This session is being continued from a previous conversation')) {
|
|
147
|
+
return {
|
|
148
|
+
kind: 'notice',
|
|
149
|
+
notice: { kind: 'compact', label: 'Earlier conversation summarised', body: src },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// A frame we do not know, with nothing but tags inside it, is machinery too.
|
|
154
|
+
if (/^<[a-z][\w-]*>/.test(src) && !/[^\s<>/\w-]/.test(src.replace(/<[^>]*>/g, ''))) {
|
|
155
|
+
return { kind: 'hidden' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { kind: 'text', text: src };
|
|
159
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indianic/dialout",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Dialout agent — reach your office, home or build machine from any browser or phone. Remote tmux terminals, live port checks, filesystem browsing, process control and HTTP tunnels. The agent dials out: no inbound ports, no VPN, no port forwarding.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dialout": "./bin/devdash-agent.js"
|