@indianic/dialout 1.11.0 → 1.12.1

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 (77) hide show
  1. package/README.md +14 -0
  2. package/dist/ai-sessions.d.ts +45 -1
  3. package/dist/ai-sessions.d.ts.map +1 -1
  4. package/dist/ai-sessions.js +68 -10
  5. package/dist/ai-sessions.js.map +1 -1
  6. package/dist/cli-client.d.ts +60 -0
  7. package/dist/cli-client.d.ts.map +1 -0
  8. package/dist/cli-client.js +246 -0
  9. package/dist/cli-client.js.map +1 -0
  10. package/dist/cli-terminal.d.ts +101 -0
  11. package/dist/cli-terminal.d.ts.map +1 -0
  12. package/dist/cli-terminal.js +1121 -0
  13. package/dist/cli-terminal.js.map +1 -0
  14. package/dist/cli.js +84 -0
  15. package/dist/cli.js.map +1 -1
  16. package/dist/file-transfer.d.ts +18 -0
  17. package/dist/file-transfer.d.ts.map +1 -0
  18. package/dist/file-transfer.js +266 -0
  19. package/dist/file-transfer.js.map +1 -0
  20. package/dist/fs-browser.d.ts +3 -1
  21. package/dist/fs-browser.d.ts.map +1 -1
  22. package/dist/fs-browser.js +35 -6
  23. package/dist/fs-browser.js.map +1 -1
  24. package/dist/mcp-stdio.d.ts +10 -0
  25. package/dist/mcp-stdio.d.ts.map +1 -0
  26. package/dist/mcp-stdio.js +199 -0
  27. package/dist/mcp-stdio.js.map +1 -0
  28. package/dist/shared-shell.d.ts +77 -0
  29. package/dist/shared-shell.d.ts.map +1 -0
  30. package/dist/shared-shell.js +282 -0
  31. package/dist/shared-shell.js.map +1 -0
  32. package/dist/websocket.d.ts.map +1 -1
  33. package/dist/websocket.js +413 -1
  34. package/dist/websocket.js.map +1 -1
  35. package/node_modules/@indianic/dialout-shared/dist/captured.d.ts +27 -0
  36. package/node_modules/@indianic/dialout-shared/dist/captured.d.ts.map +1 -0
  37. package/node_modules/@indianic/dialout-shared/dist/captured.js +23 -0
  38. package/node_modules/@indianic/dialout-shared/dist/captured.test.d.ts +2 -0
  39. package/node_modules/@indianic/dialout-shared/dist/captured.test.d.ts.map +1 -0
  40. package/node_modules/@indianic/dialout-shared/dist/captured.test.js +27 -0
  41. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts +5 -0
  42. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts.map +1 -1
  43. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.js +14 -3
  44. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.js +14 -0
  45. package/node_modules/@indianic/dialout-shared/dist/event-merge.d.ts +7 -0
  46. package/node_modules/@indianic/dialout-shared/dist/event-merge.d.ts.map +1 -1
  47. package/node_modules/@indianic/dialout-shared/dist/event-merge.test.js +24 -0
  48. package/node_modules/@indianic/dialout-shared/dist/index.d.ts +5 -1
  49. package/node_modules/@indianic/dialout-shared/dist/index.d.ts.map +1 -1
  50. package/node_modules/@indianic/dialout-shared/dist/index.js +11 -1
  51. package/node_modules/@indianic/dialout-shared/dist/notices.d.ts +37 -0
  52. package/node_modules/@indianic/dialout-shared/dist/notices.d.ts.map +1 -0
  53. package/node_modules/@indianic/dialout-shared/dist/notices.js +124 -0
  54. package/node_modules/@indianic/dialout-shared/dist/notices.test.d.ts +2 -0
  55. package/node_modules/@indianic/dialout-shared/dist/notices.test.d.ts.map +1 -0
  56. package/node_modules/@indianic/dialout-shared/dist/notices.test.js +92 -0
  57. package/node_modules/@indianic/dialout-shared/dist/notify.d.ts +9 -0
  58. package/node_modules/@indianic/dialout-shared/dist/notify.d.ts.map +1 -1
  59. package/node_modules/@indianic/dialout-shared/dist/notify.js +27 -0
  60. package/node_modules/@indianic/dialout-shared/dist/session-announce.d.ts +20 -0
  61. package/node_modules/@indianic/dialout-shared/dist/session-announce.d.ts.map +1 -0
  62. package/node_modules/@indianic/dialout-shared/dist/session-announce.js +20 -0
  63. package/node_modules/@indianic/dialout-shared/dist/session-announce.test.d.ts +2 -0
  64. package/node_modules/@indianic/dialout-shared/dist/session-announce.test.d.ts.map +1 -0
  65. package/node_modules/@indianic/dialout-shared/dist/session-announce.test.js +30 -0
  66. package/node_modules/@indianic/dialout-shared/package.json +2 -2
  67. package/node_modules/@indianic/dialout-shared/src/captured.test.ts +29 -0
  68. package/node_modules/@indianic/dialout-shared/src/captured.ts +46 -0
  69. package/node_modules/@indianic/dialout-shared/src/chat-blocks.test.ts +14 -0
  70. package/node_modules/@indianic/dialout-shared/src/chat-blocks.ts +12 -3
  71. package/node_modules/@indianic/dialout-shared/src/event-merge.test.ts +27 -0
  72. package/node_modules/@indianic/dialout-shared/src/event-merge.ts +11 -0
  73. package/node_modules/@indianic/dialout-shared/src/index.ts +14 -1
  74. package/node_modules/@indianic/dialout-shared/src/notices.test.ts +102 -0
  75. package/node_modules/@indianic/dialout-shared/src/notices.ts +159 -0
  76. package/node_modules/@indianic/dialout-shared/src/notify.ts +51 -0
  77. package/package.json +2 -2
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const captured_1 = require("./captured");
5
+ (0, vitest_1.describe)('parseCapturePage', () => {
6
+ (0, vitest_1.it)('defaults to the newest full page', () => {
7
+ (0, vitest_1.expect)((0, captured_1.parseCapturePage)(null, null)).toEqual({ before: null, limit: captured_1.MAX_CAPTURE_PAGE });
8
+ });
9
+ (0, vitest_1.it)('caps the page so one request cannot pull a whole conversation', () => {
10
+ (0, vitest_1.expect)((0, captured_1.parseCapturePage)(null, String(captured_1.MAX_CAPTURE_PAGE * 10)).limit).toBe(captured_1.MAX_CAPTURE_PAGE);
11
+ (0, vitest_1.expect)((0, captured_1.parseCapturePage)(null, '25').limit).toBe(25);
12
+ });
13
+ (0, vitest_1.it)('treats junk, zero and negative limits as the default', () => {
14
+ for (const v of ['0', '-5', 'all', '', 'NaN', null, undefined]) {
15
+ (0, vitest_1.expect)((0, captured_1.parseCapturePage)(null, v).limit).toBe(captured_1.MAX_CAPTURE_PAGE);
16
+ }
17
+ });
18
+ (0, vitest_1.it)('reads `before` only as a positive whole index', () => {
19
+ (0, vitest_1.expect)((0, captured_1.parseCapturePage)('450', null).before).toBe(450);
20
+ // seq 0 is the first event ever; "before 0" is empty, which is the same
21
+ // request as "the newest page" would be wrong — so it is not honoured.
22
+ (0, vitest_1.expect)((0, captured_1.parseCapturePage)('0', null).before).toBeNull();
23
+ (0, vitest_1.expect)((0, captured_1.parseCapturePage)('-1', null).before).toBeNull();
24
+ (0, vitest_1.expect)((0, captured_1.parseCapturePage)('12.5', null).before).toBeNull();
25
+ (0, vitest_1.expect)((0, captured_1.parseCapturePage)('abc', null).before).toBeNull();
26
+ });
27
+ });
@@ -1,4 +1,5 @@
1
1
  import type { AiEvent } from './events';
2
+ import { type MessageNotice } from './notices';
2
3
  export interface ToolItem {
3
4
  id: string;
4
5
  name: string;
@@ -25,6 +26,10 @@ export type ChatBlock = {
25
26
  key: string;
26
27
  text: string;
27
28
  ms?: number;
29
+ } | {
30
+ kind: 'notice';
31
+ key: string;
32
+ notice: MessageNotice;
28
33
  } | {
29
34
  kind: 'status';
30
35
  key: string;
@@ -1 +1 @@
1
- {"version":3,"file":"chat-blocks.d.ts","sourceRoot":"","sources":["../src/chat-blocks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAM,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAK,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAI,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAIvD,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,EAAE,CA0D1D;AAKD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAE9D"}
1
+ {"version":3,"file":"chat-blocks.d.ts","sourceRoot":"","sources":["../src/chat-blocks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAEhE,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAM,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAK,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAI,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAI,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAIvD,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,EAAE,CAiE1D;AAKD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAE9D"}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.groupEvents = groupEvents;
4
4
  exports.toolGroupStartsOpen = toolGroupStartsOpen;
5
+ const notices_1 = require("./notices");
5
6
  // Grouping only ever merges ADJACENT events, so the worst case for an adapter
6
7
  // that interleaves is two blocks instead of one — never a reordering.
7
8
  function groupEvents(events) {
@@ -11,9 +12,19 @@ function groupEvents(events) {
11
12
  const key = `${('id' in e && e.id) || ('forId' in e && e.forId) || e.kind}-${i}`;
12
13
  const tail = blocks[blocks.length - 1];
13
14
  if (e.kind === 'message') {
14
- blocks.push(e.role === 'user'
15
- ? { kind: 'user', key, text: e.text || '' }
16
- : { kind: 'assistant', key, text: e.text || '' });
15
+ if (e.role !== 'user') {
16
+ blocks.push({ kind: 'assistant', key, text: e.text || '' });
17
+ return;
18
+ }
19
+ // Half the "user" messages in an agent transcript are the harness
20
+ // talking to itself. Read the frame before drawing a bubble for it.
21
+ const read = (0, notices_1.readUserMessage)(e.text || '');
22
+ if (read.kind === 'hidden')
23
+ return;
24
+ if (read.kind === 'notice')
25
+ blocks.push({ kind: 'notice', key, notice: read.notice });
26
+ else
27
+ blocks.push({ kind: 'user', key, text: read.text });
17
28
  return;
18
29
  }
19
30
  if (e.kind === 'tool_call') {
@@ -49,6 +49,20 @@ const msg = (role, text, id = text) => ({ kind: 'message', role, text, id });
49
49
  const blocks = (0, chat_blocks_1.groupEvents)([msg('user', 'hi'), msg('assistant', 'hello')]);
50
50
  (0, vitest_1.expect)(blocks.map((b) => b.kind)).toEqual(['user', 'assistant']);
51
51
  });
52
+ (0, vitest_1.it)('turns a harness message into a notice, and drops a pure reminder', () => {
53
+ const blocks = (0, chat_blocks_1.groupEvents)([
54
+ msg('user', 'ship it'),
55
+ msg('user', '<system-reminder>read the rules again</system-reminder>'),
56
+ msg('user', '<task-notification><summary>Agent "Review" finished</summary>'
57
+ + '<usage><tool_uses>8</tool_uses><duration_ms>77347</duration_ms></usage></task-notification>'),
58
+ ]);
59
+ (0, vitest_1.expect)(blocks.map((b) => b.kind)).toEqual(['user', 'notice']);
60
+ const notice = blocks[1];
61
+ if (notice.kind !== 'notice')
62
+ throw new Error('expected a notice');
63
+ (0, vitest_1.expect)(notice.notice.label).toBe('Agent "Review" finished');
64
+ (0, vitest_1.expect)(notice.notice.detail).toBe('1m 17s · 8 tools');
65
+ });
52
66
  (0, vitest_1.it)('gives every block a stable unique key', () => {
53
67
  const blocks = (0, chat_blocks_1.groupEvents)([msg('user', 'hi'), call('a', 'Read'), call('b', 'Bash')]);
54
68
  const keys = blocks.map((b) => b.key);
@@ -10,6 +10,13 @@ export interface TranscriptState {
10
10
  export interface EventsMeta {
11
11
  replay?: boolean;
12
12
  history?: boolean;
13
+ /**
14
+ * This message answers an open that declared `haveThrough`: it carries only
15
+ * what came after it. Merged as an append, but unlike a live batch it must
16
+ * not disturb `earlier` — the client's own count came from the database and
17
+ * reaches further back than the agent's in-memory window does.
18
+ */
19
+ delta?: boolean;
13
20
  firstSeq?: number;
14
21
  earlier?: number;
15
22
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"event-merge.d.ts","sourceRoot":"","sources":["../src/event-merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAiBxC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7E;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CAC5B;AAED,eAAO,MAAM,gBAAgB,EAAE,eAA4D,CAAC;AAE5F,wBAAgB,WAAW,CAAC,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,GAAE,UAAe,GAAG,eAAe,CA0B/G"}
1
+ {"version":3,"file":"event-merge.d.ts","sourceRoot":"","sources":["../src/event-merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAqBxC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,gFAAgF;IAChF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7E;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;CAC5B;AAED,eAAO,MAAM,gBAAgB,EAAE,eAA4D,CAAC;AAE5F,wBAAgB,WAAW,CAAC,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,IAAI,GAAE,UAAe,GAAG,eAAe,CA0B/G"}
@@ -31,6 +31,30 @@ const texts = (s) => s.events.map((e) => e.text);
31
31
  (0, vitest_1.expect)(texts(s)).toEqual(['m0', 'm1', 'm2', 'm3', 'm4']);
32
32
  (0, vitest_1.expect)((0, event_merge_1.mergeEvents)(s, range(3, 5), { firstSeq: 3 })).toBe(s);
33
33
  });
34
+ (0, vitest_1.it)('a delta appends onto the captured copy and leaves `earlier` alone', () => {
35
+ // What the session screen painted from the server's database: 200 events
36
+ // ending at seq 449, with 250 older ones still stored behind them.
37
+ let s = (0, event_merge_1.mergeEvents)(event_merge_1.EMPTY_TRANSCRIPT, range(250, 450), { replay: true, firstSeq: 250, earlier: 250 });
38
+ // The open declared haveThrough: 449; the agent answers with 450 onwards.
39
+ s = (0, event_merge_1.mergeEvents)(s, range(450, 453), { delta: true, firstSeq: 450 });
40
+ (0, vitest_1.expect)(texts(s)).toHaveLength(203);
41
+ (0, vitest_1.expect)(texts(s)[202]).toBe('m452');
42
+ (0, vitest_1.expect)(s.firstSeq).toBe(250);
43
+ // The agent's window reaches back less far than the database does, so it
44
+ // sends no `earlier` and the client's own count must survive.
45
+ (0, vitest_1.expect)(s.earlier).toBe(250);
46
+ });
47
+ (0, vitest_1.it)('an empty delta is the in-sync signal and changes nothing', () => {
48
+ const s = (0, event_merge_1.mergeEvents)(event_merge_1.EMPTY_TRANSCRIPT, range(0, 3), { replay: true, firstSeq: 0, earlier: 7 });
49
+ (0, vitest_1.expect)((0, event_merge_1.mergeEvents)(s, [], { delta: true, firstSeq: 3 })).toBe(s);
50
+ });
51
+ (0, vitest_1.it)('a delta that overlaps the tail is trimmed, not doubled', () => {
52
+ let s = (0, event_merge_1.mergeEvents)(event_merge_1.EMPTY_TRANSCRIPT, range(0, 5), { replay: true, firstSeq: 0, earlier: 0 });
53
+ // A race: the live tail already delivered m5 before the open was answered.
54
+ s = (0, event_merge_1.mergeEvents)(s, range(5, 6), { firstSeq: 5 });
55
+ s = (0, event_merge_1.mergeEvents)(s, range(3, 7), { delta: true, firstSeq: 3 });
56
+ (0, vitest_1.expect)(texts(s)).toEqual(['m0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6']);
57
+ });
34
58
  (0, vitest_1.it)('an agent without paging is a plain append', () => {
35
59
  let s = (0, event_merge_1.mergeEvents)(event_merge_1.EMPTY_TRANSCRIPT, range(0, 2));
36
60
  s = (0, event_merge_1.mergeEvents)(s, range(2, 3));
@@ -7,9 +7,11 @@ export type { AiSessionSummary } from './session';
7
7
  export type { AiUsage, AiRateWindow } from './usage';
8
8
  export { REPLAY_LIMIT } from './session';
9
9
  export type { CommandSource, AiCommand, McpServerInfo, AiCapabilities } from './capabilities';
10
- export { NOTIFY_COOLDOWN_MS, shouldNotifyAi, shouldNotifyPrompt } from './notify';
10
+ export { NOTIFY_COOLDOWN_MS, shouldNotifyAi, shouldNotifyPrompt, shouldNotifyNewTerminal, shouldNotifyNewAiSession, type PushPrefs, } from './notify';
11
11
  export type { ToolItem, ChatBlock } from './chat-blocks';
12
12
  export { groupEvents, toolGroupStartsOpen } from './chat-blocks';
13
+ export type { MessageNotice, NoticeKind, ReadMessage } from './notices';
14
+ export { dedent, readUserMessage } from './notices';
13
15
  export { PIN_THRESHOLD_PX, shouldFollow } from './scroll-pin';
14
16
  export type { ToolClass, ToolAppearance } from './tool-appearance';
15
17
  export { toolClass, toolAppearance } from './tool-appearance';
@@ -23,6 +25,8 @@ export type { TmuxPrefs, TmuxStatusPrefs } from './tmux-prefs';
23
25
  export { DEFAULT_TMUX_PREFS, STATUS_TOKENS, MAX_STATUS_TEMPLATE, compileStatusTemplate, isValidStatusTemplate, isValidTmuxColor, resolveTmuxPrefs, } from './tmux-prefs';
24
26
  export type { TranscriptState, EventsMeta } from './event-merge';
25
27
  export { EMPTY_TRANSCRIPT, mergeEvents } from './event-merge';
28
+ export type { CapturedTranscript } from './captured';
29
+ export { EMPTY_CAPTURE, MAX_CAPTURE_PAGE, parseCapturePage } from './captured';
26
30
  export { thinkingDuration, thinkingLabel } from './thinking';
27
31
  export type { PanePrompt, PanePromptOption, PromptKey } from './pane-prompt';
28
32
  export { PROMPT_KEYS, detectPanePrompt, freeTextRow, isFreeTextOption, isSafeKeyList, promptKeys, promptMoveKeys } from './pane-prompt';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAElD,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAElE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEzC,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE9F,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAElF,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEjE,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE9D,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE9D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE9D,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEjF,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAErG,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAExH,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EACL,kBAAkB,EAAE,aAAa,EAAE,mBAAmB,EACtD,qBAAqB,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,gBAAgB,GACjF,MAAM,cAAc,CAAC;AAEtB,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE9D,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAExI,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EACL,cAAc,EAAE,oBAAoB,EAAE,oBAAoB,EAC1D,sBAAsB,EAAE,WAAW,EAAE,kBAAkB,EAAE,gBAAgB,GAC1E,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.splitAttachments = exports.safeAttachmentName = exports.isImageName = exports.composeWithAttachments = exports.ATTACHMENT_MAX_FILES = exports.ATTACHMENT_MAX_BYTES = exports.ATTACHMENT_DIR = exports.promptMoveKeys = exports.promptKeys = exports.isSafeKeyList = exports.isFreeTextOption = exports.freeTextRow = exports.detectPanePrompt = exports.PROMPT_KEYS = exports.thinkingLabel = exports.thinkingDuration = exports.mergeEvents = exports.EMPTY_TRANSCRIPT = exports.resolveTmuxPrefs = exports.isValidTmuxColor = exports.isValidStatusTemplate = exports.compileStatusTemplate = exports.MAX_STATUS_TEMPLATE = exports.STATUS_TOKENS = exports.DEFAULT_TMUX_PREFS = exports.renderTerminalName = exports.factsFromSession = exports.TERMINAL_NAME_TOKENS = exports.DEFAULT_TERMINAL_TEMPLATE = exports.setFunctionKeysVisible = exports.getFunctionKeysVisible = exports.FUNCTION_KEYS_PREF = exports.shouldSubmitOnEnter = exports.COARSE_POINTER_QUERY = exports.rankCommands = exports.commandQuery = exports.toolAppearance = exports.toolClass = exports.shouldFollow = exports.PIN_THRESHOLD_PX = exports.toolGroupStartsOpen = exports.groupEvents = exports.shouldNotifyPrompt = exports.shouldNotifyAi = exports.NOTIFY_COOLDOWN_MS = exports.REPLAY_LIMIT = exports.isPermissionMode = exports.PERMISSION_MODES = exports.preview = exports.PREVIEW_LIMIT = void 0;
3
+ exports.promptMoveKeys = exports.promptKeys = exports.isSafeKeyList = exports.isFreeTextOption = exports.freeTextRow = exports.detectPanePrompt = exports.PROMPT_KEYS = exports.thinkingLabel = exports.thinkingDuration = exports.parseCapturePage = exports.MAX_CAPTURE_PAGE = exports.EMPTY_CAPTURE = exports.mergeEvents = exports.EMPTY_TRANSCRIPT = exports.resolveTmuxPrefs = exports.isValidTmuxColor = exports.isValidStatusTemplate = exports.compileStatusTemplate = exports.MAX_STATUS_TEMPLATE = exports.STATUS_TOKENS = exports.DEFAULT_TMUX_PREFS = exports.renderTerminalName = exports.factsFromSession = exports.TERMINAL_NAME_TOKENS = exports.DEFAULT_TERMINAL_TEMPLATE = exports.setFunctionKeysVisible = exports.getFunctionKeysVisible = exports.FUNCTION_KEYS_PREF = exports.shouldSubmitOnEnter = exports.COARSE_POINTER_QUERY = exports.rankCommands = exports.commandQuery = exports.toolAppearance = exports.toolClass = exports.shouldFollow = exports.PIN_THRESHOLD_PX = exports.readUserMessage = exports.dedent = exports.toolGroupStartsOpen = exports.groupEvents = exports.shouldNotifyNewAiSession = exports.shouldNotifyNewTerminal = exports.shouldNotifyPrompt = exports.shouldNotifyAi = exports.NOTIFY_COOLDOWN_MS = exports.REPLAY_LIMIT = exports.isPermissionMode = exports.PERMISSION_MODES = exports.preview = exports.PREVIEW_LIMIT = void 0;
4
+ exports.splitAttachments = exports.safeAttachmentName = exports.isImageName = exports.composeWithAttachments = exports.ATTACHMENT_MAX_FILES = exports.ATTACHMENT_MAX_BYTES = exports.ATTACHMENT_DIR = void 0;
4
5
  var events_1 = require("./events");
5
6
  Object.defineProperty(exports, "PREVIEW_LIMIT", { enumerable: true, get: function () { return events_1.PREVIEW_LIMIT; } });
6
7
  Object.defineProperty(exports, "preview", { enumerable: true, get: function () { return events_1.preview; } });
@@ -13,9 +14,14 @@ var notify_1 = require("./notify");
13
14
  Object.defineProperty(exports, "NOTIFY_COOLDOWN_MS", { enumerable: true, get: function () { return notify_1.NOTIFY_COOLDOWN_MS; } });
14
15
  Object.defineProperty(exports, "shouldNotifyAi", { enumerable: true, get: function () { return notify_1.shouldNotifyAi; } });
15
16
  Object.defineProperty(exports, "shouldNotifyPrompt", { enumerable: true, get: function () { return notify_1.shouldNotifyPrompt; } });
17
+ Object.defineProperty(exports, "shouldNotifyNewTerminal", { enumerable: true, get: function () { return notify_1.shouldNotifyNewTerminal; } });
18
+ Object.defineProperty(exports, "shouldNotifyNewAiSession", { enumerable: true, get: function () { return notify_1.shouldNotifyNewAiSession; } });
16
19
  var chat_blocks_1 = require("./chat-blocks");
17
20
  Object.defineProperty(exports, "groupEvents", { enumerable: true, get: function () { return chat_blocks_1.groupEvents; } });
18
21
  Object.defineProperty(exports, "toolGroupStartsOpen", { enumerable: true, get: function () { return chat_blocks_1.toolGroupStartsOpen; } });
22
+ var notices_1 = require("./notices");
23
+ Object.defineProperty(exports, "dedent", { enumerable: true, get: function () { return notices_1.dedent; } });
24
+ Object.defineProperty(exports, "readUserMessage", { enumerable: true, get: function () { return notices_1.readUserMessage; } });
19
25
  var scroll_pin_1 = require("./scroll-pin");
20
26
  Object.defineProperty(exports, "PIN_THRESHOLD_PX", { enumerable: true, get: function () { return scroll_pin_1.PIN_THRESHOLD_PX; } });
21
27
  Object.defineProperty(exports, "shouldFollow", { enumerable: true, get: function () { return scroll_pin_1.shouldFollow; } });
@@ -48,6 +54,10 @@ Object.defineProperty(exports, "resolveTmuxPrefs", { enumerable: true, get: func
48
54
  var event_merge_1 = require("./event-merge");
49
55
  Object.defineProperty(exports, "EMPTY_TRANSCRIPT", { enumerable: true, get: function () { return event_merge_1.EMPTY_TRANSCRIPT; } });
50
56
  Object.defineProperty(exports, "mergeEvents", { enumerable: true, get: function () { return event_merge_1.mergeEvents; } });
57
+ var captured_1 = require("./captured");
58
+ Object.defineProperty(exports, "EMPTY_CAPTURE", { enumerable: true, get: function () { return captured_1.EMPTY_CAPTURE; } });
59
+ Object.defineProperty(exports, "MAX_CAPTURE_PAGE", { enumerable: true, get: function () { return captured_1.MAX_CAPTURE_PAGE; } });
60
+ Object.defineProperty(exports, "parseCapturePage", { enumerable: true, get: function () { return captured_1.parseCapturePage; } });
51
61
  var thinking_1 = require("./thinking");
52
62
  Object.defineProperty(exports, "thinkingDuration", { enumerable: true, get: function () { return thinking_1.thinkingDuration; } });
53
63
  Object.defineProperty(exports, "thinkingLabel", { enumerable: true, get: function () { return thinking_1.thinkingLabel; } });
@@ -0,0 +1,37 @@
1
+ export type NoticeKind = 'task' | 'handback' | 'session' | 'command' | 'output' | 'compact';
2
+ export interface MessageNotice {
3
+ kind: NoticeKind;
4
+ /** The single line, the way the terminal would print it. */
5
+ label: string;
6
+ /** What the line earns a suffix for: a duration, a tool count, an id. */
7
+ detail?: string;
8
+ /** Markdown worth keeping behind a toggle; absent means nothing to open. */
9
+ body?: string;
10
+ }
11
+ export type ReadMessage = {
12
+ kind: 'text';
13
+ text: string;
14
+ } | {
15
+ kind: 'notice';
16
+ notice: MessageNotice;
17
+ } | {
18
+ kind: 'hidden';
19
+ };
20
+ /**
21
+ * Strip the indent a frame added, so the markdown inside survives.
22
+ *
23
+ * The hand-back frame indents every line of a subagent's report by two
24
+ * spaces — deliberately, so a forged frame line at column zero is visible.
25
+ * Left in place that indent turns the report's `- ` bullets into continuation
26
+ * lines and its fenced blocks into something the parser never reaches.
27
+ */
28
+ export declare function dedent(src: string): string;
29
+ /**
30
+ * What to draw for one user-role message.
31
+ *
32
+ * Order matters: a task notification usually arrives wrapped in a reminder,
33
+ * and a slash command carries its own stdout, so the frames are peeled before
34
+ * anything is matched and the more specific shape is tested first.
35
+ */
36
+ export declare function readUserMessage(raw: string): ReadMessage;
37
+ //# sourceMappingURL=notices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notices.d.ts","sourceRoot":"","sources":["../src/notices.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5F,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAYvB;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAU1C;AA0CD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CA+CxD"}
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dedent = dedent;
4
+ exports.readUserMessage = readUserMessage;
5
+ const thinking_1 = require("./thinking");
6
+ /** Reminders are a frame around other content as often as they are content. */
7
+ const REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
8
+ const CAVEAT = /<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g;
9
+ const HANDBACK_FRAME = 'The report follows:';
10
+ function tag(src, name) {
11
+ const m = new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(src);
12
+ return m ? m[1].trim() : '';
13
+ }
14
+ /**
15
+ * Strip the indent a frame added, so the markdown inside survives.
16
+ *
17
+ * The hand-back frame indents every line of a subagent's report by two
18
+ * spaces — deliberately, so a forged frame line at column zero is visible.
19
+ * Left in place that indent turns the report's `- ` bullets into continuation
20
+ * lines and its fenced blocks into something the parser never reaches.
21
+ */
22
+ function dedent(src) {
23
+ const lines = String(src || '').replace(/\r\n?/g, '\n').split('\n')
24
+ .map((l) => l.replace(/\t/g, ' '));
25
+ let min = Infinity;
26
+ for (const l of lines) {
27
+ if (!l.trim())
28
+ continue;
29
+ min = Math.min(min, /^ */.exec(l)[0].length);
30
+ }
31
+ if (!isFinite(min) || min === 0)
32
+ return lines.join('\n').trim();
33
+ return lines.map((l) => (l.trim() ? l.slice(min) : '')).join('\n').trim();
34
+ }
35
+ function taskNotice(src) {
36
+ const n = src.slice(src.indexOf('<task-notification>'));
37
+ const summary = tag(n, 'summary');
38
+ const status = tag(n, 'status');
39
+ const ms = Number(tag(n, 'duration_ms') || 0);
40
+ const tools = Number(tag(n, 'tool_uses') || 0);
41
+ const bits = [];
42
+ if (ms > 0)
43
+ bits.push((0, thinking_1.thinkingDuration)(ms));
44
+ if (tools > 0)
45
+ bits.push(`${tools} tool${tools === 1 ? '' : 's'}`);
46
+ // The result is usually a pointer to the report rather than the report, and
47
+ // the note explains the lifecycle. Both are worth keeping, neither is worth
48
+ // a line of its own.
49
+ const body = [tag(n, 'result'), tag(n, 'note')].filter(Boolean).join('\n\n');
50
+ return {
51
+ kind: 'task',
52
+ label: summary || (status ? `Background task ${status}` : 'Background task'),
53
+ detail: bits.join(' · ') || undefined,
54
+ body: body || undefined,
55
+ };
56
+ }
57
+ function handbackNotice(src, from) {
58
+ const open = /<agent-message from="([^"]*)">/.exec(src);
59
+ let body = open ? src.slice((open.index ?? 0) + open[0].length) : src;
60
+ body = body.replace(/<\/agent-message>\s*$/, '');
61
+ const handback = body.includes('[Subagent hand-back]');
62
+ // Everything before "The report follows:" is the harness explaining that
63
+ // the report carries no user authority. True, and already enforced by the
64
+ // fact that we render it as a notice rather than as a request.
65
+ const at = body.indexOf(HANDBACK_FRAME);
66
+ if (at >= 0)
67
+ body = body.slice(at + HANDBACK_FRAME.length);
68
+ const who = (from || '').trim();
69
+ return {
70
+ kind: handback ? 'handback' : 'session',
71
+ label: handback ? 'Report from a delegated agent' : 'Message from another session',
72
+ detail: who || undefined,
73
+ body: dedent(body) || undefined,
74
+ };
75
+ }
76
+ /**
77
+ * What to draw for one user-role message.
78
+ *
79
+ * Order matters: a task notification usually arrives wrapped in a reminder,
80
+ * and a slash command carries its own stdout, so the frames are peeled before
81
+ * anything is matched and the more specific shape is tested first.
82
+ */
83
+ function readUserMessage(raw) {
84
+ const full = String(raw || '').replace(/\r\n?/g, '\n');
85
+ const src = full.replace(REMINDER, '\n').replace(CAVEAT, '\n').trim();
86
+ if (!src)
87
+ return { kind: 'hidden' };
88
+ if (src.includes('<task-notification>')) {
89
+ return { kind: 'notice', notice: taskNotice(src) };
90
+ }
91
+ const from = /<agent-message from="([^"]*)">/.exec(src);
92
+ if (from || src.startsWith('Another Claude session sent a message')) {
93
+ return { kind: 'notice', notice: handbackNotice(src, from ? from[1] : '') };
94
+ }
95
+ if (src.includes('<command-name>')) {
96
+ const name = tag(src, 'command-name');
97
+ const args = tag(src, 'command-args');
98
+ const out = tag(src, 'local-command-stdout');
99
+ return {
100
+ kind: 'notice',
101
+ notice: {
102
+ kind: 'command',
103
+ label: [name || 'Command', args].filter(Boolean).join(' '),
104
+ body: out || undefined,
105
+ },
106
+ };
107
+ }
108
+ if (src.includes('<local-command-stdout>')) {
109
+ const out = tag(src, 'local-command-stdout');
110
+ return { kind: 'notice', notice: { kind: 'output', label: 'Command output', body: out || undefined } };
111
+ }
112
+ if (src.includes('<tools-used-before-summary>')
113
+ || src.startsWith('This session is being continued from a previous conversation')) {
114
+ return {
115
+ kind: 'notice',
116
+ notice: { kind: 'compact', label: 'Earlier conversation summarised', body: src },
117
+ };
118
+ }
119
+ // A frame we do not know, with nothing but tags inside it, is machinery too.
120
+ if (/^<[a-z][\w-]*>/.test(src) && !/[^\s<>/\w-]/.test(src.replace(/<[^>]*>/g, ''))) {
121
+ return { kind: 'hidden' };
122
+ }
123
+ return { kind: 'text', text: src };
124
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=notices.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notices.test.d.ts","sourceRoot":"","sources":["../src/notices.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const notices_1 = require("./notices");
5
+ // The samples below are real messages lifted from a Claude Code transcript,
6
+ // trimmed only in length. Inventing their shape is how the first version of
7
+ // this got the tag names wrong.
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
+ const HANDBACK = `Another Claude session sent a message:
24
+ <agent-message from="a0db7a3ef39d95a28">
25
+ [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:
26
+ **Re-review 1 verdict: Ready to merge.**
27
+
28
+ - **C1 (profiles lost on reconnect) is fixed.**
29
+ - No leak across owners or runner ids.
30
+ </agent-message>`;
31
+ (0, vitest_1.describe)('readUserMessage', () => {
32
+ (0, vitest_1.it)('reduces a task notification to the line the terminal prints', () => {
33
+ const read = (0, notices_1.readUserMessage)(TASK);
34
+ (0, vitest_1.expect)(read.kind).toBe('notice');
35
+ if (read.kind !== 'notice')
36
+ return;
37
+ (0, vitest_1.expect)(read.notice.kind).toBe('task');
38
+ (0, vitest_1.expect)(read.notice.label).toBe('Agent "Fix SPCP final review findings" finished');
39
+ (0, vitest_1.expect)(read.notice.detail).toBe('18m 45s · 92 tools');
40
+ (0, vitest_1.expect)(read.notice.body).toContain('has not reported yet');
41
+ });
42
+ (0, vitest_1.it)('keeps a hand-back report as markdown, without the authority frame', () => {
43
+ const read = (0, notices_1.readUserMessage)(HANDBACK);
44
+ (0, vitest_1.expect)(read.kind).toBe('notice');
45
+ if (read.kind !== 'notice')
46
+ return;
47
+ (0, vitest_1.expect)(read.notice.kind).toBe('handback');
48
+ (0, vitest_1.expect)(read.notice.label).toBe('Report from a delegated agent');
49
+ (0, vitest_1.expect)(read.notice.detail).toBe('a0db7a3ef39d95a28');
50
+ // The frame is gone, the markers survive for the markdown renderer, and
51
+ // the two-space harness indent is off so the bullets are bullets.
52
+ (0, vitest_1.expect)(read.notice.body).toBe('**Re-review 1 verdict: Ready to merge.**\n\n'
53
+ + '- **C1 (profiles lost on reconnect) is fixed.**\n'
54
+ + '- No leak across owners or runner ids.');
55
+ (0, vitest_1.expect)(read.notice.body).not.toContain('NOT a message from the user');
56
+ });
57
+ (0, vitest_1.it)('hides a message that is only a system reminder', () => {
58
+ (0, vitest_1.expect)((0, notices_1.readUserMessage)('<system-reminder>Read CLAUDE.md again.</system-reminder>').kind).toBe('hidden');
59
+ (0, vitest_1.expect)((0, notices_1.readUserMessage)(' ').kind).toBe('hidden');
60
+ });
61
+ (0, vitest_1.it)('keeps the person\'s own words, reminder stripped', () => {
62
+ const read = (0, notices_1.readUserMessage)('push the build\n<system-reminder>be careful</system-reminder>');
63
+ (0, vitest_1.expect)(read).toEqual({ kind: 'text', text: 'push the build' });
64
+ });
65
+ (0, vitest_1.it)('shows a slash command as the command, with its output foldable', () => {
66
+ const read = (0, notices_1.readUserMessage)('<command-message>clear</command-message><command-name>/clear</command-name>'
67
+ + '<command-args>--all</command-args><local-command-stdout>done</local-command-stdout>');
68
+ (0, vitest_1.expect)(read.kind).toBe('notice');
69
+ if (read.kind !== 'notice')
70
+ return;
71
+ (0, vitest_1.expect)(read.notice.label).toBe('/clear --all');
72
+ (0, vitest_1.expect)(read.notice.body).toBe('done');
73
+ });
74
+ (0, vitest_1.it)('names a resumed conversation rather than printing the summary', () => {
75
+ const read = (0, notices_1.readUserMessage)('This session is being continued from a previous conversation. Summary: lots.');
76
+ (0, vitest_1.expect)(read.kind).toBe('notice');
77
+ if (read.kind !== 'notice')
78
+ return;
79
+ (0, vitest_1.expect)(read.notice.label).toBe('Earlier conversation summarised');
80
+ });
81
+ (0, vitest_1.it)('leaves ordinary text alone, markers and all', () => {
82
+ (0, vitest_1.expect)((0, notices_1.readUserMessage)('fix **this** please')).toEqual({ kind: 'text', text: 'fix **this** please' });
83
+ });
84
+ });
85
+ (0, vitest_1.describe)('dedent', () => {
86
+ (0, vitest_1.it)('removes the common indent and nothing else', () => {
87
+ (0, vitest_1.expect)((0, notices_1.dedent)(' a\n b\n\n c')).toBe('a\n b\n\nc');
88
+ });
89
+ (0, vitest_1.it)('leaves unindented text untouched', () => {
90
+ (0, vitest_1.expect)((0, notices_1.dedent)('a\n b')).toBe('a\n b');
91
+ });
92
+ });
@@ -7,4 +7,13 @@ export declare function shouldNotifyAi(previous: string | undefined, next: strin
7
7
  * Still not on first sight (reconnect bursts) and still rate-limited.
8
8
  */
9
9
  export declare function shouldNotifyPrompt(previousPromptId: string | null | undefined, nextPromptId: string | null | undefined, lastNotifiedAt: number | undefined, nowMs: number): boolean;
10
+ /** Which events a user wants pushed. One flag per event, no nesting. */
11
+ export interface PushPrefs {
12
+ aiQuestions: boolean;
13
+ newTerminal: boolean;
14
+ newAiSession: boolean;
15
+ announcements: boolean;
16
+ }
17
+ export declare function shouldNotifyNewTerminal(prefs: PushPrefs, subscriberInstallId: string | null, createdByInstallId: string | null, lastNotifiedAt: number | undefined, nowMs: number): boolean;
18
+ export declare function shouldNotifyNewAiSession(prefs: PushPrefs, subscriberInstallId: string | null, createdByInstallId: string | null, lastNotifiedAt: number | undefined, nowMs: number): boolean;
10
19
  //# sourceMappingURL=notify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAI1C,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,KAAK,EAAE,MAAM,GACZ,OAAO,CAcT;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC3C,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACvC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,KAAK,EAAE,MAAM,GACZ,OAAO,CAKT"}
1
+ {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAI1C,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,KAAK,EAAE,MAAM,GACZ,OAAO,CAcT;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC3C,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACvC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,KAAK,EAAE,MAAM,GACZ,OAAO,CAKT;AAED,wEAAwE;AACxE,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB;AAyBD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,SAAS,EAChB,mBAAmB,EAAE,MAAM,GAAG,IAAI,EAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,EACjC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,KAAK,EAAE,MAAM,GACZ,OAAO,CAET;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,SAAS,EAChB,mBAAmB,EAAE,MAAM,GAAG,IAAI,EAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,EACjC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,KAAK,EAAE,MAAM,GACZ,OAAO,CAET"}
@@ -8,6 +8,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.NOTIFY_COOLDOWN_MS = void 0;
9
9
  exports.shouldNotifyAi = shouldNotifyAi;
10
10
  exports.shouldNotifyPrompt = shouldNotifyPrompt;
11
+ exports.shouldNotifyNewTerminal = shouldNotifyNewTerminal;
12
+ exports.shouldNotifyNewAiSession = shouldNotifyNewAiSession;
11
13
  exports.NOTIFY_COOLDOWN_MS = 120_000;
12
14
  const WAITING = new Set(['waiting_input', 'waiting_approval']);
13
15
  function shouldNotifyAi(previous, next, lastNotifiedAt, nowMs) {
@@ -45,3 +47,28 @@ function shouldNotifyPrompt(previousPromptId, nextPromptId, lastNotifiedAt, nowM
45
47
  return false;
46
48
  return true;
47
49
  }
50
+ /**
51
+ * Per-subscription, not per-session: whether a session is worth announcing
52
+ * depends on which device is being told.
53
+ *
54
+ * `createdByInstallId` is null for a native tmux shell the agent merely found,
55
+ * which is the case most worth announcing — so a null creator always notifies,
56
+ * whatever the subscriber knows about itself. An unknown subscriber (one
57
+ * registered before install ids existed) also notifies: a missed notification
58
+ * is a worse failure than one extra.
59
+ */
60
+ function notifyNewSession(enabled, subscriberInstallId, createdByInstallId, lastNotifiedAt, nowMs) {
61
+ if (!enabled)
62
+ return false;
63
+ if (createdByInstallId !== null && subscriberInstallId === createdByInstallId)
64
+ return false;
65
+ if (lastNotifiedAt && nowMs - lastNotifiedAt < exports.NOTIFY_COOLDOWN_MS)
66
+ return false;
67
+ return true;
68
+ }
69
+ function shouldNotifyNewTerminal(prefs, subscriberInstallId, createdByInstallId, lastNotifiedAt, nowMs) {
70
+ return notifyNewSession(prefs.newTerminal, subscriberInstallId, createdByInstallId, lastNotifiedAt, nowMs);
71
+ }
72
+ function shouldNotifyNewAiSession(prefs, subscriberInstallId, createdByInstallId, lastNotifiedAt, nowMs) {
73
+ return notifyNewSession(prefs.newAiSession, subscriberInstallId, createdByInstallId, lastNotifiedAt, nowMs);
74
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Whether a tmux session the agent just reported is news.
3
+ *
4
+ * The bug this exists to kill: the daemon's close handler used to set
5
+ * `ended_at` on every session, so a reconnect made the registry's UPDATE miss,
6
+ * take the INSERT branch, and push "New terminal" for every terminal that was
7
+ * already open. Task 2 stops writing `ended_at` there; this is the second
8
+ * line of defence, for a session the vanish-pass legitimately ended and that
9
+ * then came back.
10
+ */
11
+ export interface PriorSession {
12
+ /** Null while the session is live. */
13
+ endedAt: string | null;
14
+ /** When tmux created the session, as the agent reported it. */
15
+ createdLocal: string | null;
16
+ /** When a push for this row went out, or null if none ever did. */
17
+ notifiedAt: string | null;
18
+ }
19
+ export declare function shouldAnnounceSession(prior: PriorSession | null, createdLocal: string | null): boolean;
20
+ //# sourceMappingURL=session-announce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-announce.d.ts","sourceRoot":"","sources":["../src/session-announce.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,sCAAsC;IACtC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mEAAmE;IACnE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,YAAY,GAAG,IAAI,EAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,GAC1B,OAAO,CAeT"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ // packages/devdash-shared/src/session-announce.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.shouldAnnounceSession = shouldAnnounceSession;
5
+ function shouldAnnounceSession(prior, createdLocal) {
6
+ // Never seen on this machine under this name: unambiguously new.
7
+ if (!prior)
8
+ return true;
9
+ // Both creation times known and different: tmux really did make a new
10
+ // session that happens to reuse the name.
11
+ if (prior.createdLocal && createdLocal && prior.createdLocal !== createdLocal)
12
+ return true;
13
+ // Everything else is the same session returning — including the case where
14
+ // either creation time is missing. The original push spec said a missed
15
+ // notification is worse than an extra one; for this trigger that is
16
+ // backwards, because the failure being fixed is a burst. The cost is that an
17
+ // agent too old to report a creation time cannot announce a genuinely new
18
+ // session that reuses an old name.
19
+ return false;
20
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session-announce.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-announce.test.d.ts","sourceRoot":"","sources":["../src/session-announce.test.ts"],"names":[],"mappings":""}