@itookit/dsht 0.3.8 → 0.5.2

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 (166) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +33 -12
  3. package/README.zh.md +35 -14
  4. package/dist/catalog/controller.d.ts +26 -6
  5. package/dist/catalog/controller.js +73 -45
  6. package/dist/catalog/index.d.ts +1 -0
  7. package/dist/cli/dsht.js +206 -18
  8. package/dist/cli/startup.d.ts +40 -0
  9. package/dist/cli/startup.js +314 -0
  10. package/dist/cli/trace-summary.d.ts +78 -0
  11. package/dist/cli/trace-summary.js +241 -0
  12. package/dist/cli/verifier.d.ts +64 -0
  13. package/dist/cli/verifier.js +265 -0
  14. package/dist/contracts.d.ts +359 -0
  15. package/dist/contracts.js +1 -0
  16. package/dist/controller/commands.d.ts +47 -0
  17. package/dist/controller/commands.js +322 -0
  18. package/dist/controller/connection-streams.d.ts +22 -0
  19. package/dist/controller/connection-streams.js +105 -0
  20. package/dist/controller/connection.d.ts +24 -31
  21. package/dist/controller/connection.js +48 -111
  22. package/dist/controller/controller.d.ts +412 -178
  23. package/dist/controller/controller.js +713 -167
  24. package/dist/controller/foreground.d.ts +44 -0
  25. package/dist/controller/foreground.js +79 -0
  26. package/dist/controller/index.d.ts +8 -1
  27. package/dist/controller/index.js +5 -0
  28. package/dist/controller/loop-contract.d.ts +136 -0
  29. package/dist/controller/loop-contract.js +308 -0
  30. package/dist/controller/loop-coordinator.d.ts +48 -0
  31. package/dist/controller/loop-coordinator.js +647 -0
  32. package/dist/controller/loop-prompts-schema.d.ts +56 -0
  33. package/dist/controller/loop-prompts-schema.js +144 -0
  34. package/dist/controller/loop-prompts.d.ts +55 -0
  35. package/dist/controller/loop-prompts.generated.d.ts +104 -0
  36. package/dist/controller/loop-prompts.generated.js +185 -0
  37. package/dist/controller/loop-prompts.js +104 -0
  38. package/dist/controller/loop-protocols.d.ts +39 -0
  39. package/dist/controller/loop-protocols.js +115 -0
  40. package/dist/controller/loop.d.ts +275 -0
  41. package/dist/controller/loop.js +378 -0
  42. package/dist/controller/prompts.d.ts +54 -0
  43. package/dist/controller/prompts.js +162 -0
  44. package/dist/controller/trace-log.d.ts +45 -0
  45. package/dist/controller/trace-log.js +144 -0
  46. package/dist/controller/verifier.d.ts +130 -0
  47. package/dist/controller/verifier.js +75 -0
  48. package/dist/cost/controller.d.ts +1 -1
  49. package/dist/cost/controller.js +12 -5
  50. package/dist/cost/index.d.ts +1 -1
  51. package/dist/cost/index.js +1 -1
  52. package/dist/cost/ledger.d.ts +0 -1
  53. package/dist/cost/ledger.js +0 -1
  54. package/dist/cost/scanner.js +1 -0
  55. package/dist/json.d.ts +18 -0
  56. package/dist/json.js +19 -0
  57. package/dist/references.d.ts +25 -0
  58. package/dist/references.js +26 -0
  59. package/dist/session/connection-view.d.ts +2 -11
  60. package/dist/session/controller.d.ts +94 -105
  61. package/dist/session/controller.js +262 -536
  62. package/dist/session/history-reader.d.ts +32 -0
  63. package/dist/session/history-reader.js +170 -0
  64. package/dist/session/history.d.ts +6 -18
  65. package/dist/session/history.js +1 -24
  66. package/dist/session/index.d.ts +9 -4
  67. package/dist/session/index.js +7 -3
  68. package/dist/session/info.d.ts +20 -82
  69. package/dist/session/info.js +52 -25
  70. package/dist/session/interactions.d.ts +26 -0
  71. package/dist/session/interactions.js +75 -0
  72. package/dist/session/markdown.js +1 -1
  73. package/dist/session/math.js +1 -1
  74. package/dist/session/mutation-gate.d.ts +51 -0
  75. package/dist/session/mutation-gate.js +73 -0
  76. package/dist/session/navigation.d.ts +2 -89
  77. package/dist/session/navigation.js +2 -129
  78. package/dist/session/navigator.d.ts +47 -0
  79. package/dist/session/navigator.js +158 -0
  80. package/dist/session/peek.d.ts +38 -0
  81. package/dist/session/peek.js +103 -0
  82. package/dist/session/prompt-backfill.d.ts +23 -0
  83. package/dist/session/prompt-backfill.js +88 -0
  84. package/dist/session/references.d.ts +2 -20
  85. package/dist/session/references.js +1 -26
  86. package/dist/session/runtime.d.ts +26 -0
  87. package/dist/session/runtime.js +28 -0
  88. package/dist/session/state.d.ts +20 -0
  89. package/dist/session/state.js +1 -0
  90. package/dist/session/telemetry.d.ts +25 -17
  91. package/dist/session/telemetry.js +66 -60
  92. package/dist/session/transcript.d.ts +5 -7
  93. package/dist/session/transcript.js +2 -15
  94. package/dist/session/types.d.ts +25 -0
  95. package/dist/session/types.js +0 -1
  96. package/dist/session-title.d.ts +9 -0
  97. package/dist/session-title.js +21 -0
  98. package/dist/shell/controller.d.ts +31 -1
  99. package/dist/shell/controller.js +34 -2
  100. package/dist/shell/index.d.ts +3 -3
  101. package/dist/shell/index.js +2 -2
  102. package/dist/shell/runner.d.ts +10 -0
  103. package/dist/shell/runner.js +48 -9
  104. package/dist/slash/index.d.ts +10 -0
  105. package/dist/slash/index.js +7 -0
  106. package/dist/slash/parse.d.ts +42 -0
  107. package/dist/slash/parse.js +259 -0
  108. package/dist/slash/pipeline.d.ts +140 -0
  109. package/dist/slash/pipeline.js +115 -0
  110. package/dist/slash/registry.d.ts +88 -0
  111. package/dist/slash/registry.js +177 -0
  112. package/dist/slash/types.d.ts +126 -0
  113. package/dist/slash/types.js +1 -0
  114. package/dist/state.d.ts +16 -18
  115. package/dist/state.js +4 -3
  116. package/dist/text.d.ts +28 -0
  117. package/dist/text.js +55 -0
  118. package/dist/transport/client.d.ts +4 -3
  119. package/dist/transport/client.js +71 -25
  120. package/dist/transport/events.d.ts +104 -0
  121. package/dist/transport/events.js +149 -0
  122. package/dist/transport/wire.d.ts +9 -17
  123. package/dist/transport/wire.js +2 -27
  124. package/dist/ui/app.js +750 -550
  125. package/dist/ui/chat/header.js +1 -1
  126. package/dist/ui/chat/history-view.d.ts +1 -1
  127. package/dist/ui/chat/loop-status.d.ts +11 -0
  128. package/dist/ui/chat/loop-status.js +28 -0
  129. package/dist/ui/chat/navigation-model.d.ts +86 -0
  130. package/dist/ui/chat/navigation-model.js +107 -0
  131. package/dist/ui/chat/shell-view.d.ts +17 -2
  132. package/dist/ui/chat/shell-view.js +45 -3
  133. package/dist/ui/chat/status.d.ts +47 -3
  134. package/dist/ui/chat/status.js +65 -50
  135. package/dist/ui/chat/use-history-view.d.ts +69 -0
  136. package/dist/ui/chat/use-history-view.js +123 -0
  137. package/dist/ui/chat/viewport.d.ts +1 -1
  138. package/dist/ui/dialogs/cost.d.ts +21 -4
  139. package/dist/ui/dialogs/cost.js +7 -12
  140. package/dist/ui/dialogs/index.d.ts +22 -5
  141. package/dist/ui/dialogs/index.js +19 -3
  142. package/dist/ui/dialogs/loop.d.ts +43 -0
  143. package/dist/ui/dialogs/loop.js +224 -0
  144. package/dist/ui/dialogs/peek.d.ts +25 -0
  145. package/dist/ui/dialogs/peek.js +35 -0
  146. package/dist/ui/dialogs/picker.d.ts +2 -0
  147. package/dist/ui/dialogs/picker.js +4 -2
  148. package/dist/ui/dialogs/use-panels.d.ts +53 -0
  149. package/dist/ui/dialogs/use-panels.js +51 -0
  150. package/dist/ui/input/mouse.d.ts +12 -2
  151. package/dist/ui/input/mouse.js +20 -7
  152. package/dist/ui/input/references.d.ts +1 -1
  153. package/dist/ui/input/use-composer.d.ts +35 -0
  154. package/dist/ui/input/use-composer.js +109 -0
  155. package/dist/ui/input/use-deferred-lines.d.ts +16 -0
  156. package/dist/ui/input/use-deferred-lines.js +54 -0
  157. package/dist/ui/input/use-history-recall.d.ts +20 -0
  158. package/dist/ui/input/use-history-recall.js +47 -0
  159. package/dist/ui/status/model.d.ts +7 -0
  160. package/dist/ui/status/model.js +5 -0
  161. package/dist/ui/theme/index.d.ts +1 -1
  162. package/package.json +6 -4
  163. package/dist/ui/commands/parse.d.ts +0 -104
  164. package/dist/ui/commands/parse.js +0 -135
  165. package/dist/ui/commands/registry.d.ts +0 -33
  166. package/dist/ui/commands/registry.js +0 -73
@@ -0,0 +1,32 @@
1
+ import type { HostAccess } from '../transport/host.ts';
2
+ import { Transcript } from './transcript.ts';
3
+ import type { HistorySearch } from './types.ts';
4
+ interface Selection {
5
+ sessionId: string;
6
+ revision: number;
7
+ record: Transcript;
8
+ }
9
+ /** History reads cannot navigate, change status, or write another domain's state. */
10
+ export interface HistoryHost extends Pick<HostAccess, 'require' | 'online' | 'signal'> {
11
+ selected(): Selection;
12
+ current(selection: Selection): boolean;
13
+ owns(transcript: Transcript): boolean;
14
+ changed(): void;
15
+ }
16
+ export declare class HistoryReader {
17
+ private readonly host;
18
+ private readonly tasks;
19
+ constructor(host: HistoryHost);
20
+ /** A selection change cancels everything; closing one detached window cancels only its pages. */
21
+ cancel(source?: Transcript): void;
22
+ settle(): Promise<void>;
23
+ private read;
24
+ waitForHistory(signal: AbortSignal): Promise<void>;
25
+ older(signal?: AbortSignal, transcript?: Transcript): Promise<void>;
26
+ private page;
27
+ historyThrough(target: number | 'first', signal: AbortSignal): Promise<void>;
28
+ searchHistory(query: string, signal: AbortSignal): Promise<HistorySearch>;
29
+ /** The returned window belongs to the caller until it is installed as the reading view. */
30
+ historyAt(target: number, signal: AbortSignal): Promise<Transcript>;
31
+ }
32
+ export {};
@@ -0,0 +1,170 @@
1
+ /** Selected-session history I/O, with cancellation tied to the record or window being read. */
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { object } from "../json.js";
4
+ import { toolLine } from "../text.js";
5
+ import { Transcript } from "./transcript.js";
6
+ const SEARCH_MATCH_LIMIT = 200;
7
+ export class HistoryReader {
8
+ host;
9
+ tasks = new Set();
10
+ constructor(host) {
11
+ this.host = host;
12
+ }
13
+ /** A selection change cancels everything; closing one detached window cancels only its pages. */
14
+ cancel(source) {
15
+ for (const task of this.tasks)
16
+ if (source === undefined || task.source === source)
17
+ task.abort.abort();
18
+ }
19
+ async settle() { await Promise.allSettled([...this.tasks].map(task => task.promise)); }
20
+ read(caller, source, work, discard) {
21
+ const abort = new AbortController();
22
+ const signal = AbortSignal.any([abort.signal, this.host.signal(), ...(caller ? [caller] : [])]);
23
+ // Register before dispatch so even a synchronous observer can cancel this operation.
24
+ const task = { abort, source, promise: Promise.resolve() };
25
+ const promise = Promise.resolve().then(async () => {
26
+ signal.throwIfAborted();
27
+ const selected = this.host.selected();
28
+ const check = () => {
29
+ signal.throwIfAborted();
30
+ if (!this.host.online() || !this.host.current(selected))
31
+ throw new Error('Session changed while reading history');
32
+ if (source !== undefined && !this.host.owns(source))
33
+ throw new Error('History window closed');
34
+ };
35
+ check();
36
+ const value = await work({ ...selected, client: this.host.require(), signal, check });
37
+ try {
38
+ check();
39
+ return value;
40
+ }
41
+ catch (error) {
42
+ discard?.(value);
43
+ throw error;
44
+ }
45
+ }).finally(() => { this.tasks.delete(task); });
46
+ task.promise = promise;
47
+ this.tasks.add(task);
48
+ return promise;
49
+ }
50
+ waitForHistory(signal) {
51
+ return this.read(signal, undefined, async (context) => {
52
+ const deadline = Date.now() + context.client.timeoutMs;
53
+ while (!context.record.ready) {
54
+ context.check();
55
+ if (Date.now() >= deadline)
56
+ throw new Error('Session snapshot timed out');
57
+ await delay(20, undefined, { signal: context.signal });
58
+ }
59
+ context.check();
60
+ });
61
+ }
62
+ older(signal, transcript) {
63
+ return this.read(signal, transcript, context => this.page(context, transcript ?? context.record));
64
+ }
65
+ async page(context, transcript) {
66
+ context.check();
67
+ if (!transcript.ready || !transcript.hasMore || transcript.beforeSeq === undefined)
68
+ return;
69
+ const before = transcript.beforeSeq;
70
+ const result = await context.client.call('session/page', { request: {
71
+ address: { kind: 'session', sessionId: context.sessionId }, throughSeq: transcript.cursor,
72
+ beforeSeq: before, maxMessages: 80,
73
+ } }, context.signal);
74
+ context.check();
75
+ if (!this.host.owns(transcript))
76
+ throw new Error('History window closed');
77
+ transcript.addPage(result);
78
+ this.host.changed();
79
+ if (transcript.hasMore && (transcript.beforeSeq === undefined || transcript.beforeSeq >= before)) {
80
+ throw new Error('Host history page did not advance');
81
+ }
82
+ }
83
+ historyThrough(target, signal) {
84
+ return this.read(signal, undefined, async (context) => {
85
+ const transcript = context.record;
86
+ if (!transcript.ready)
87
+ throw new Error('Wait for the session snapshot');
88
+ while (transcript.hasMore && (target === 'first' || transcript.beforeSeq !== undefined && transcript.beforeSeq > target)) {
89
+ await this.page(context, transcript);
90
+ }
91
+ });
92
+ }
93
+ searchHistory(query, signal) {
94
+ return this.read(signal, undefined, async (context) => {
95
+ const source = context.record;
96
+ if (!source.ready)
97
+ throw new Error('Wait for the session snapshot');
98
+ const throughSeq = source.readThrough;
99
+ const needle = query.toLowerCase();
100
+ const result = { items: [], truncated: false };
101
+ const scan = (transcript) => {
102
+ const messages = transcript.messages;
103
+ for (let index = messages.length - 1; index >= 0; index--) {
104
+ context.check();
105
+ const message = messages[index];
106
+ if (message.role === 'Tool')
107
+ continue;
108
+ const text = message.text;
109
+ const match = text.toLowerCase().indexOf(needle);
110
+ if (match < 0)
111
+ continue;
112
+ if (result.items.length === SEARCH_MATCH_LIMIT) {
113
+ result.truncated = true;
114
+ return false;
115
+ }
116
+ result.items.push({ seq: message.seq, role: message.role,
117
+ preview: Buffer.from(toolLine(text.slice(Math.max(0, match - 40), match + needle.length + 100), 160)).toString('utf8') });
118
+ }
119
+ return true;
120
+ };
121
+ if (!scan(source))
122
+ return result;
123
+ let beforeSeq = source.beforeSeq;
124
+ let hasMore = source.hasMore;
125
+ while (hasMore && beforeSeq !== undefined) {
126
+ const page = object(await context.client.call('session/page', { request: {
127
+ address: { kind: 'session', sessionId: context.sessionId }, throughSeq, beforeSeq, maxMessages: 80,
128
+ } }, context.signal));
129
+ context.check();
130
+ const temporary = new Transcript();
131
+ try {
132
+ temporary.accept({ type: 'snapshot', cursor: throughSeq, assistantStream: { revision: 0 }, records: page.records, hasMore: page.hasMore });
133
+ const next = temporary.beforeSeq;
134
+ if (temporary.hasMore && (next === undefined || next >= beforeSeq))
135
+ throw new Error('Host history page did not advance');
136
+ if (!scan(temporary))
137
+ return result;
138
+ beforeSeq = next;
139
+ hasMore = temporary.hasMore;
140
+ }
141
+ finally {
142
+ temporary.dispose();
143
+ }
144
+ }
145
+ return result;
146
+ });
147
+ }
148
+ /** The returned window belongs to the caller until it is installed as the reading view. */
149
+ historyAt(target, signal) {
150
+ return this.read(signal, undefined, async (context) => {
151
+ const throughSeq = context.record.readThrough;
152
+ const page = object(await context.client.call('session/page', { request: {
153
+ address: { kind: 'session', sessionId: context.sessionId }, throughSeq,
154
+ beforeSeq: target + 1, maxMessages: 80,
155
+ } }, context.signal));
156
+ context.check();
157
+ const window = new Transcript();
158
+ try {
159
+ window.accept({ type: 'snapshot', cursor: throughSeq, assistantStream: { revision: 0 }, records: page.records, hasMore: page.hasMore });
160
+ if (!window.messages.some(message => message.seq === target))
161
+ throw new Error('The host did not return the requested message');
162
+ return window;
163
+ }
164
+ catch (error) {
165
+ window.dispose();
166
+ throw error;
167
+ }
168
+ }, window => window.dispose());
169
+ }
170
+ }
@@ -14,24 +14,6 @@ export interface HistoryRow {
14
14
  /** Draw the row on the theme's local-command bar, used by `!` commands. */
15
15
  highlight?: boolean;
16
16
  }
17
- /** Wrap text into rows with the per-kind trimming rule.
18
- * @param text - Text to wrap.
19
- * @param width - Available terminal columns.
20
- * @param kind - Part kind, which decides whether wrapped lines are trimmed.
21
- * @param seq - Durable sequence, for committed parts.
22
- * @returns The wrapped rows.
23
- */
24
- /** Wrap plain local text into terminal rows, preserving its own whitespace.
25
- *
26
- * Command output is aligned by spaces and indented by stack traces, so this never collapses runs of
27
- * whitespace the way tool summaries do; only the terminal width decides where a row breaks.
28
- * @param text - Raw text, possibly containing newlines.
29
- * @param width - Available terminal columns.
30
- * @param kind - Row kind used for coloring.
31
- * @param highlight - Whether the rows are a local command line drawn on the command bar.
32
- * @returns One row per wrapped terminal line; empty text yields one empty row.
33
- */
34
- export declare function plainRows(text: string, width: number, kind: RowKind, highlight?: boolean): HistoryRow[];
35
17
  /** Drop all terminal rows and layout metadata for an evicted or inactive transcript.
36
18
  * @param transcript - Transcript whose previously returned layout is no longer used.
37
19
  */
@@ -59,6 +41,12 @@ export declare function layoutStats(transcript: Transcript): {
59
41
  * @param liveReasoning - Fold mode for completed live blocks; below 60 content columns it also folds active reasoning.
60
42
  * @returns Row count, sequence offsets, and a viewport reader; `lines` materializes all rows for exports only.
61
43
  */
44
+ /** Plain view of one laid-out record: the projection engine's only public product.
45
+ *
46
+ * It exposes rows through `viewport` and message positions through `offsets`, so a caller can render
47
+ * a window without holding the record itself.
48
+ */
49
+ export type SessionRender = ReturnType<typeof historyLayout>;
62
50
  export declare function historyLayout(transcript: Transcript, width: number, reasoning?: Reasoning, overrides?: ReadonlySet<number>, liveReasoning?: Reasoning): {
63
51
  messages: Message[];
64
52
  offsets: Map<number, number>;
@@ -1,7 +1,7 @@
1
1
  /** Indexed semantic history with bounded terminal-row caching and viewport-only materialization. */
2
2
  import wrapAnsi from 'wrap-ansi';
3
- import { toolLine } from "./transcript.js";
4
3
  import { hasMarkdown, markdownRows } from "./markdown.js";
4
+ import { toolLine } from "../text.js";
5
5
  /** A session owns one layout cache; dropped sessions release their entire cache. */
6
6
  class LayoutIndex {
7
7
  width;
@@ -140,21 +140,6 @@ const liveRows = new WeakMap();
140
140
  * @param seq - Durable sequence, for committed parts.
141
141
  * @returns The wrapped rows.
142
142
  */
143
- /** Wrap plain local text into terminal rows, preserving its own whitespace.
144
- *
145
- * Command output is aligned by spaces and indented by stack traces, so this never collapses runs of
146
- * whitespace the way tool summaries do; only the terminal width decides where a row breaks.
147
- * @param text - Raw text, possibly containing newlines.
148
- * @param width - Available terminal columns.
149
- * @param kind - Row kind used for coloring.
150
- * @param highlight - Whether the rows are a local command line drawn on the command bar.
151
- * @returns One row per wrapped terminal line; empty text yields one empty row.
152
- */
153
- export function plainRows(text, width, kind, highlight = false) {
154
- const columns = Math.max(1, width);
155
- const wrapped = wrapAnsi(text === '' ? ' ' : text, columns, { hard: true, trim: false });
156
- return wrapped.split('\n').map(line => ({ text: line, kind, ...(highlight ? { highlight: true } : {}) }));
157
- }
158
143
  function wrapRows(text, width, kind, seq) {
159
144
  return wrapAnsi(text, width, { hard: true, trim: !['tool', 'success', 'error'].includes(kind) })
160
145
  .split('\n').map(text => ({ text, kind, seq }));
@@ -295,14 +280,6 @@ export function layoutStats(transcript) {
295
280
  return undefined;
296
281
  return { ...index.stats(), liveWraps: index.liveWraps.size, liveMarkdown: index.liveMarkdown.size };
297
282
  }
298
- /** Lay out an indexed conversation without concatenating its historical rows on every stream frame.
299
- * @param transcript - Selected session's semantic content and unfinished assistant output.
300
- * @param width - Available terminal columns.
301
- * @param reasoning - Global committed/completed reasoning fold mode.
302
- * @param overrides - Sequences whose fold mode differs from the global mode; replace the set on changes.
303
- * @param liveReasoning - Fold mode for completed live blocks; below 60 content columns it also folds active reasoning.
304
- * @returns Row count, sequence offsets, and a viewport reader; `lines` materializes all rows for exports only.
305
- */
306
283
  export function historyLayout(transcript, width, reasoning = 'row', overrides = noOverrides, liveReasoning = reasoning) {
307
284
  let index = indexes.get(transcript);
308
285
  if (!index || index.width !== width || index.reasoning !== reasoning || index.overrides !== overrides) {
@@ -1,6 +1,9 @@
1
1
  /** Session domain: the selected session, its transcript, layout, telemetry and navigation. */
2
2
  export { SessionController } from './controller.ts';
3
- export { contentText, toolLine, Transcript } from './transcript.ts';
3
+ export { SessionMutationGate } from './mutation-gate.ts';
4
+ export type { MutationAdmission, MutationLane } from './mutation-gate.ts';
5
+ export { contentText, Transcript } from './transcript.ts';
6
+ export { toolLine } from '../text.ts';
4
7
  export type { LivePhase, Message, MessagePart, ThoughtEntry } from './transcript.ts';
5
8
  export { historyLayout, layoutStats, releaseHistoryLayout } from './history.ts';
6
9
  export { markdownCacheStats } from './markdown.ts';
@@ -10,9 +13,11 @@ export type { QueuedInput } from './telemetry.ts';
10
13
  export { DEFAULT_HISTORY_LIMITS, historyLimits } from './memory.ts';
11
14
  export type { HistoryLimits } from './memory.ts';
12
15
  export { DEFAULT_PROMPT_LIMITS, promptText, PromptIndex, SessionInfo } from './info.ts';
13
- export type { ComposerState, InteractionState, ModelState, OptionState, PanelState, PromptEntry, PromptLimits, PromptRecord, ReferenceState, ViewState } from './info.ts';
14
- export { navigationCommand, resolveTarget, sessionLabel } from './navigation.ts';
15
- export { activeReference, fileMention, fileReferences } from './references.ts';
16
+ export type { InteractionState, OptionState, PromptEntry, PromptLimits, PromptRecord, } from './info.ts';
17
+ export { resolveTarget } from './navigation.ts';
18
+ export { sessionLabel } from '../session-title.ts';
19
+ export { fileReferences } from './references.ts';
20
+ export { activeReference, fileMention } from '../references.ts';
16
21
  export type { FileReference } from './references.ts';
17
22
  export { saveSessionLog } from './export.ts';
18
23
  export type { HistorySearch, RemovalTarget } from './types.ts';
@@ -1,11 +1,15 @@
1
1
  /** Session domain: the selected session, its transcript, layout, telemetry and navigation. */
2
2
  export { SessionController } from "./controller.js";
3
- export { contentText, toolLine, Transcript } from "./transcript.js";
3
+ export { SessionMutationGate } from "./mutation-gate.js";
4
+ export { contentText, Transcript } from "./transcript.js";
5
+ export { toolLine } from "../text.js";
4
6
  export { historyLayout, layoutStats, releaseHistoryLayout } from "./history.js";
5
7
  export { markdownCacheStats } from "./markdown.js";
6
8
  export { Telemetry } from "./telemetry.js";
7
9
  export { DEFAULT_HISTORY_LIMITS, historyLimits } from "./memory.js";
8
10
  export { DEFAULT_PROMPT_LIMITS, promptText, PromptIndex, SessionInfo } from "./info.js";
9
- export { navigationCommand, resolveTarget, sessionLabel } from "./navigation.js";
10
- export { activeReference, fileMention, fileReferences } from "./references.js";
11
+ export { resolveTarget } from "./navigation.js";
12
+ export { sessionLabel } from "../session-title.js";
13
+ export { fileReferences } from "./references.js";
14
+ export { activeReference, fileMention } from "../references.js";
11
15
  export { saveSessionLog } from "./export.js";
@@ -1,23 +1,6 @@
1
- /** Session-owned prompt index: every user prompt of the selected session, plus its recall cursor.
2
- *
3
- * The index replaces a plain bounded recall buffer. Each durable entry carries the sequence it came
4
- * from, so an entry evicted by the budgets stays recoverable: the loaded window refills anything the
5
- * transcript still holds, and `session/page` refetches anything older than the window. Budgets
6
- * therefore bound memory without deciding reachability — which is what a base-200 buffer got wrong.
7
- *
8
- * Locally submitted slash commands never become durable records, so they are retained too, marked as
9
- * non-durable. A durable echo of a locally recorded prompt upgrades that entry instead of adding a
10
- * second copy, keeping the refill boundary (the oldest durable sequence) exact.
11
- */
12
- import { type Reasoning } from './history.ts';
13
- import { Transcript } from './transcript.ts';
14
- import type { HistorySearch } from './types.ts';
15
- import type { ObjectValue } from '../transport/wire.ts';
16
- /** A durable user prompt as the transcript reports it, before retention. */
17
- export interface PromptRecord {
18
- seq: number;
19
- text: string;
20
- }
1
+ import { Transcript, type PromptRecord } from './transcript.ts';
2
+ export type { PromptRecord } from './transcript.ts';
3
+ import type { AnswerValue } from './types.ts';
21
4
  /** One retained prompt: its text, plus the durable sequence when the host also recorded it. */
22
5
  export interface PromptEntry extends PromptRecord {
23
6
  durable: boolean;
@@ -31,61 +14,6 @@ export interface PromptLimits {
31
14
  export declare const DEFAULT_PROMPT_LIMITS: PromptLimits;
32
15
  /** Flatten one transcript prompt into the single line the composer recalls. */
33
16
  export declare function promptText(value: string): string;
34
- /** The selected session's composer: its draft, the caret in it, and a draft a dialog parked aside. */
35
- export interface ComposerState {
36
- draft: string;
37
- cursor: number;
38
- parked: string;
39
- }
40
- /** How the selected session's record is being read: which window, where, and what is expanded.
41
- *
42
- * Everything here changes how the record renders, which is why it is session state rather than a
43
- * transient panel flag; the record's content stays in `Transcript`. `window` is a strong reference
44
- * released by `closeWindow`, and the layout cache keyed by it is a weak one.
45
- */
46
- export interface ViewState {
47
- /** Detached record shown instead of the live transcript while reading jumped-to history. */
48
- window?: Transcript;
49
- scroll: number;
50
- /** Reading protection: reclamation pauses until the reader returns to the live end. */
51
- pinned: boolean;
52
- /** Message sequences whose reasoning is expanded beyond the default fold. */
53
- folds: ReadonlySet<number>;
54
- /** Fold mode for the live attempt's completed reasoning. */
55
- liveReasoning: Reasoning;
56
- }
57
- /** Composer-adjacent `@` reference menu: the highlighted row and the draft that dismissed it. */
58
- export interface ReferenceState {
59
- index: number;
60
- dismissed?: string;
61
- }
62
- /** Model dialog step: the catalog plus the provider or model being inspected. */
63
- export interface ModelState {
64
- catalog: ObjectValue;
65
- provider?: string;
66
- model?: ObjectValue;
67
- }
68
- /** Panels the reader opened for the selected session.
69
- *
70
- * Only visibility and query text: every panel's rows come from the record, so closing one loses
71
- * nothing and a session switch may clear all of it. The row cursor inside a panel is not here — it
72
- * is focus, held by `Picker` and reset through its `key`.
73
- */
74
- export interface PanelState {
75
- thoughts: boolean;
76
- queue: boolean;
77
- model?: ModelState;
78
- history?: {
79
- query: string;
80
- contentSearch: boolean;
81
- matches?: HistorySearch;
82
- };
83
- search?: {
84
- query: string;
85
- items: ObjectValue[];
86
- hasMore: boolean;
87
- };
88
- }
89
17
  /** Keyboard state of one pending question's options. */
90
18
  export interface OptionState {
91
19
  key: string;
@@ -98,7 +26,7 @@ export interface OptionState {
98
26
  * `SessionController.interactions`, which is derived per session on every publish.
99
27
  */
100
28
  export interface InteractionState {
101
- answers: Record<string, ObjectValue[]>;
29
+ answers: Record<string, AnswerValue['answers']>;
102
30
  option?: OptionState;
103
31
  approval?: {
104
32
  eventId: string;
@@ -107,7 +35,7 @@ export interface InteractionState {
107
35
  }
108
36
  /** Client-owned state of the selected session, reset whenever another session is opened.
109
37
  *
110
- * It holds the record, the prompt index, the composer, the reading view and the local interaction
38
+ * It holds the record, the prompt index, the reading view and the local interaction
111
39
  * state because all five belong to one session and none of them is owned by the host beyond what the
112
40
  * record mirrors; everything derivable from `Telemetry` or `CostLedger` stays out (see the design's
113
41
  * §5.7.6). The record is referenced here and nowhere else, so "the selected session" has one entry.
@@ -117,11 +45,9 @@ export declare class SessionInfo {
117
45
  /** The selected session's record. Replaced — never mutated in place — when another session opens. */
118
46
  record: Transcript;
119
47
  readonly prompts: PromptIndex;
120
- readonly composer: ComposerState;
121
- readonly view: ViewState;
48
+ /** Detached history window the reader jumped to; released by `closeWindow` and `reset`. */
49
+ window?: Transcript;
122
50
  readonly interaction: InteractionState;
123
- readonly reference: ReferenceState;
124
- readonly panels: PanelState;
125
51
  constructor(sessionId?: string);
126
52
  /** Forget everything a previous session owned, keeping this instance identity-stable for `State`. */
127
53
  reset(sessionId?: string): void;
@@ -143,6 +69,12 @@ export declare class PromptIndex {
143
69
  private shed;
144
70
  private position;
145
71
  private draft;
72
+ /** Prompts the client sent on the operator's behalf; their durable echo never enters recall.
73
+ *
74
+ * Kept across `reset()` so re-opening the session in the same process stays clean, and bounded
75
+ * because an agent loop can send a prompt per attempt.
76
+ */
77
+ private readonly internal;
146
78
  constructor(limits?: PromptLimits);
147
79
  /** Retained entry count, so a caller can tell an empty index from a parked cursor. */
148
80
  get length(): number;
@@ -167,8 +99,14 @@ export declare class PromptIndex {
167
99
  * live window holds, so the lazy backward step could not recover them.
168
100
  */
169
101
  markComplete(): void;
170
- /** Forget one session's prompts and cursor. */
102
+ /** Forget one session's prompts and cursor; client-generated suppression is process-wide. */
171
103
  reset(): void;
104
+ /** Remember one prompt the client sent itself, so its durable echo never enters recall.
105
+ *
106
+ * An agent loop submits many turns; without this they would crowd out what the operator typed.
107
+ * @param value - Prompt text the client sent on the operator's behalf.
108
+ */
109
+ suppress(value: string): void;
172
110
  /** Fold one transcript scan: its prompts, plus the watermark it covered.
173
111
  *
174
112
  * The watermark advances even when the scan found no prompt, so a turn of assistant and tool
@@ -15,11 +15,13 @@ import { Transcript } from "./transcript.js";
15
15
  export const DEFAULT_PROMPT_LIMITS = { maxEntries: 2000, maxBytes: 512 * 1024 };
16
16
  /** Longest single prompt worth recalling; a larger one is skipped rather than truncated in place. */
17
17
  const MAX_ENTRY_CHARS = 128 * 1024;
18
+ /** Client-generated prompts remembered for recall suppression; a bounded run needs far fewer. */
19
+ const MAX_INTERNAL_PROMPTS = 512;
18
20
  /** Flatten one transcript prompt into the single line the composer recalls. */
19
21
  export function promptText(value) { return value.replace(/\r?\n/g, ' ').trim(); }
20
22
  /** Client-owned state of the selected session, reset whenever another session is opened.
21
23
  *
22
- * It holds the record, the prompt index, the composer, the reading view and the local interaction
24
+ * It holds the record, the prompt index, the reading view and the local interaction
23
25
  * state because all five belong to one session and none of them is owned by the host beyond what the
24
26
  * record mirrors; everything derivable from `Telemetry` or `CostLedger` stays out (see the design's
25
27
  * §5.7.6). The record is referenced here and nowhere else, so "the selected session" has one entry.
@@ -29,11 +31,9 @@ export class SessionInfo {
29
31
  /** The selected session's record. Replaced — never mutated in place — when another session opens. */
30
32
  record = new Transcript();
31
33
  prompts = new PromptIndex();
32
- composer = { draft: '', cursor: 0, parked: '' };
33
- view = { scroll: 0, pinned: false, folds: new Set(), liveReasoning: 'row' };
34
+ /** Detached history window the reader jumped to; released by `closeWindow` and `reset`. */
35
+ window;
34
36
  interaction = { answers: {} };
35
- reference = { index: 0 };
36
- panels = { thoughts: false, queue: false };
37
37
  constructor(sessionId = '') {
38
38
  this.sessionId = sessionId;
39
39
  }
@@ -45,32 +45,18 @@ export class SessionInfo {
45
45
  this.record.dispose();
46
46
  this.record = new Transcript();
47
47
  this.prompts.reset();
48
- this.composer.draft = '';
49
- this.composer.cursor = 0;
50
- this.composer.parked = '';
51
- this.view.scroll = 0;
52
- this.view.pinned = false;
53
- this.view.folds = new Set();
54
- this.view.liveReasoning = 'row';
55
48
  this.interaction.answers = {};
56
49
  this.interaction.option = undefined;
57
50
  this.interaction.approval = undefined;
58
- this.reference.index = 0;
59
- this.reference.dismissed = undefined;
60
- this.panels.thoughts = false;
61
- this.panels.queue = false;
62
- this.panels.model = undefined;
63
- this.panels.history = undefined;
64
- this.panels.search = undefined;
65
51
  }
66
52
  /** Release the detached history window, if the reader has one open. */
67
53
  closeWindow() {
68
- const window = this.view.window;
54
+ const window = this.window;
69
55
  if (!window)
70
56
  return;
71
57
  releaseHistoryLayout(window);
72
58
  window.dispose();
73
- this.view.window = undefined;
59
+ this.window = undefined;
74
60
  }
75
61
  }
76
62
  /** Seq-ordered prompts with a recall cursor, owned by the selected session.
@@ -88,6 +74,12 @@ export class PromptIndex {
88
74
  shed = false;
89
75
  position;
90
76
  draft = '';
77
+ /** Prompts the client sent on the operator's behalf; their durable echo never enters recall.
78
+ *
79
+ * Kept across `reset()` so re-opening the session in the same process stays clean, and bounded
80
+ * because an agent loop can send a prompt per attempt.
81
+ */
82
+ internal = new Set();
91
83
  constructor(limits = DEFAULT_PROMPT_LIMITS) {
92
84
  this.limits = limits;
93
85
  }
@@ -122,7 +114,7 @@ export class PromptIndex {
122
114
  */
123
115
  markComplete() { if (!this.shed)
124
116
  this.complete = true; }
125
- /** Forget one session's prompts and cursor. */
117
+ /** Forget one session's prompts and cursor; client-generated suppression is process-wide. */
126
118
  reset() {
127
119
  this.entries = [];
128
120
  this.bytes = 0;
@@ -132,6 +124,24 @@ export class PromptIndex {
132
124
  this.position = undefined;
133
125
  this.draft = '';
134
126
  }
127
+ /** Remember one prompt the client sent itself, so its durable echo never enters recall.
128
+ *
129
+ * An agent loop submits many turns; without this they would crowd out what the operator typed.
130
+ * @param value - Prompt text the client sent on the operator's behalf.
131
+ */
132
+ suppress(value) {
133
+ const text = promptText(value);
134
+ if (!text)
135
+ return;
136
+ this.internal.delete(text);
137
+ this.internal.add(text);
138
+ while (this.internal.size > MAX_INTERNAL_PROMPTS) {
139
+ const oldest = this.internal.values().next().value;
140
+ if (oldest === undefined)
141
+ break;
142
+ this.internal.delete(oldest);
143
+ }
144
+ }
135
145
  /** Fold one transcript scan: its prompts, plus the watermark it covered.
136
146
  *
137
147
  * The watermark advances even when the scan found no prompt, so a turn of assistant and tool
@@ -154,7 +164,10 @@ export class PromptIndex {
154
164
  * @param value - Submitted command text; consecutive repeats coalesce.
155
165
  */
156
166
  record(value) {
157
- const entry = { seq: this.newestSeq, text: promptText(value), durable: false };
167
+ const text = promptText(value);
168
+ if (this.internal.has(text))
169
+ return;
170
+ const entry = { seq: this.newestSeq, text, durable: false };
158
171
  if (!this.retainable(entry) || this.entries.at(-1)?.text === entry.text)
159
172
  return;
160
173
  this.entries.push(entry);
@@ -166,8 +179,16 @@ export class PromptIndex {
166
179
  * @returns How many entries were actually retained.
167
180
  */
168
181
  prepend(values) {
182
+ const oldest = this.oldest;
183
+ const seen = new Set();
169
184
  const older = values.map(value => ({ seq: value.seq, text: promptText(value.text), durable: true }))
170
- .filter(value => this.retainable(value));
185
+ .filter(value => {
186
+ if (oldest !== undefined && value.seq >= oldest || seen.has(value.seq)
187
+ || this.internal.has(value.text) || !this.retainable(value))
188
+ return false;
189
+ seen.add(value.seq);
190
+ return true;
191
+ });
171
192
  if (!older.length)
172
193
  return 0;
173
194
  this.entries = [...older, ...this.entries];
@@ -213,7 +234,8 @@ export class PromptIndex {
213
234
  /** Retain one durable prompt; a durable echo upgrades a local entry instead of duplicating it. */
214
235
  push(value) {
215
236
  const entry = { seq: value.seq, text: promptText(value.text), durable: true };
216
- if (!this.retainable(entry))
237
+ // A prompt the client itself sent (an agent loop) is durable history, but not composer recall.
238
+ if (this.internal.has(entry.text) || !this.retainable(entry))
217
239
  return;
218
240
  const last = this.entries.at(-1);
219
241
  if (last && last.text === entry.text) {
@@ -227,10 +249,15 @@ export class PromptIndex {
227
249
  }
228
250
  /** Drop the oldest entries until both budgets hold; a dropped prefix stays reloadable. */
229
251
  trim() {
252
+ let removed = 0;
230
253
  while (this.entries.length > this.limits.maxEntries || this.bytes > this.limits.maxBytes) {
231
254
  this.bytes -= this.entries.shift().text.length * 2;
255
+ removed++;
232
256
  this.shed = true;
257
+ this.complete = false;
233
258
  }
259
+ if (this.position !== undefined)
260
+ this.position = Math.max(0, this.position - removed);
234
261
  }
235
262
  }
236
263
  /** Bytes of prompt text the process keeps across sessions before evicting the least recently used. */