@itookit/dsht 0.3.8 → 0.5.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 (130) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +30 -11
  3. package/README.zh.md +30 -11
  4. package/dist/cli/dsht.js +203 -18
  5. package/dist/cli/startup.d.ts +40 -0
  6. package/dist/cli/startup.js +295 -0
  7. package/dist/cli/trace-summary.d.ts +78 -0
  8. package/dist/cli/trace-summary.js +241 -0
  9. package/dist/cli/verifier.d.ts +60 -0
  10. package/dist/cli/verifier.js +242 -0
  11. package/dist/contracts.d.ts +344 -0
  12. package/dist/contracts.js +1 -0
  13. package/dist/controller/commands.d.ts +47 -0
  14. package/dist/controller/commands.js +322 -0
  15. package/dist/controller/connection.d.ts +11 -29
  16. package/dist/controller/connection.js +26 -60
  17. package/dist/controller/controller.d.ts +616 -166
  18. package/dist/controller/controller.js +1395 -146
  19. package/dist/controller/index.d.ts +8 -1
  20. package/dist/controller/index.js +5 -0
  21. package/dist/controller/loop-contract.d.ts +136 -0
  22. package/dist/controller/loop-contract.js +308 -0
  23. package/dist/controller/loop-prompts-schema.d.ts +56 -0
  24. package/dist/controller/loop-prompts-schema.js +144 -0
  25. package/dist/controller/loop-prompts.d.ts +55 -0
  26. package/dist/controller/loop-prompts.generated.d.ts +104 -0
  27. package/dist/controller/loop-prompts.generated.js +185 -0
  28. package/dist/controller/loop-prompts.js +104 -0
  29. package/dist/controller/loop-protocols.d.ts +39 -0
  30. package/dist/controller/loop-protocols.js +115 -0
  31. package/dist/controller/loop.d.ts +275 -0
  32. package/dist/controller/loop.js +378 -0
  33. package/dist/controller/prompts.d.ts +54 -0
  34. package/dist/controller/prompts.js +162 -0
  35. package/dist/controller/trace-log.d.ts +45 -0
  36. package/dist/controller/trace-log.js +144 -0
  37. package/dist/controller/verifier.d.ts +126 -0
  38. package/dist/controller/verifier.js +75 -0
  39. package/dist/cost/index.d.ts +1 -1
  40. package/dist/cost/index.js +1 -1
  41. package/dist/cost/ledger.d.ts +0 -1
  42. package/dist/cost/ledger.js +0 -1
  43. package/dist/json.d.ts +18 -0
  44. package/dist/json.js +19 -0
  45. package/dist/references.d.ts +25 -0
  46. package/dist/references.js +26 -0
  47. package/dist/session/connection-view.d.ts +2 -11
  48. package/dist/session/controller.d.ts +73 -72
  49. package/dist/session/controller.js +185 -209
  50. package/dist/session/history.d.ts +6 -18
  51. package/dist/session/history.js +1 -24
  52. package/dist/session/index.d.ts +9 -4
  53. package/dist/session/index.js +7 -3
  54. package/dist/session/info.d.ts +25 -52
  55. package/dist/session/info.js +39 -25
  56. package/dist/session/markdown.js +1 -1
  57. package/dist/session/math.js +1 -1
  58. package/dist/session/mutation-gate.d.ts +51 -0
  59. package/dist/session/mutation-gate.js +73 -0
  60. package/dist/session/navigation.d.ts +2 -89
  61. package/dist/session/navigation.js +2 -129
  62. package/dist/session/peek.d.ts +38 -0
  63. package/dist/session/peek.js +103 -0
  64. package/dist/session/references.d.ts +2 -20
  65. package/dist/session/references.js +1 -26
  66. package/dist/session/runtime.d.ts +26 -0
  67. package/dist/session/runtime.js +28 -0
  68. package/dist/session/telemetry.d.ts +12 -13
  69. package/dist/session/telemetry.js +27 -58
  70. package/dist/session/transcript.d.ts +0 -6
  71. package/dist/session/transcript.js +2 -15
  72. package/dist/session/types.d.ts +25 -0
  73. package/dist/session/types.js +0 -1
  74. package/dist/session-title.d.ts +9 -0
  75. package/dist/session-title.js +21 -0
  76. package/dist/shell/controller.d.ts +31 -1
  77. package/dist/shell/controller.js +34 -2
  78. package/dist/shell/index.d.ts +3 -3
  79. package/dist/shell/index.js +2 -2
  80. package/dist/shell/runner.d.ts +10 -0
  81. package/dist/shell/runner.js +48 -9
  82. package/dist/slash/index.d.ts +10 -0
  83. package/dist/slash/index.js +7 -0
  84. package/dist/slash/parse.d.ts +166 -0
  85. package/dist/slash/parse.js +259 -0
  86. package/dist/slash/pipeline.d.ts +140 -0
  87. package/dist/slash/pipeline.js +115 -0
  88. package/dist/slash/registry.d.ts +88 -0
  89. package/dist/slash/registry.js +177 -0
  90. package/dist/state.d.ts +14 -4
  91. package/dist/state.js +3 -2
  92. package/dist/text.d.ts +28 -0
  93. package/dist/text.js +55 -0
  94. package/dist/transport/events.d.ts +104 -0
  95. package/dist/transport/events.js +149 -0
  96. package/dist/transport/wire.d.ts +9 -17
  97. package/dist/transport/wire.js +2 -27
  98. package/dist/ui/app.js +856 -441
  99. package/dist/ui/chat/header.js +1 -1
  100. package/dist/ui/chat/history-view.d.ts +1 -1
  101. package/dist/ui/chat/loop-status.d.ts +11 -0
  102. package/dist/ui/chat/loop-status.js +28 -0
  103. package/dist/ui/chat/navigation-model.d.ts +86 -0
  104. package/dist/ui/chat/navigation-model.js +107 -0
  105. package/dist/ui/chat/shell-view.d.ts +15 -2
  106. package/dist/ui/chat/shell-view.js +37 -3
  107. package/dist/ui/chat/status.d.ts +47 -3
  108. package/dist/ui/chat/status.js +65 -50
  109. package/dist/ui/chat/viewport.d.ts +1 -1
  110. package/dist/ui/dialogs/cost.d.ts +21 -4
  111. package/dist/ui/dialogs/cost.js +7 -12
  112. package/dist/ui/dialogs/index.d.ts +22 -5
  113. package/dist/ui/dialogs/index.js +19 -3
  114. package/dist/ui/dialogs/loop.d.ts +43 -0
  115. package/dist/ui/dialogs/loop.js +224 -0
  116. package/dist/ui/dialogs/peek.d.ts +25 -0
  117. package/dist/ui/dialogs/peek.js +35 -0
  118. package/dist/ui/dialogs/picker.d.ts +2 -0
  119. package/dist/ui/dialogs/picker.js +4 -2
  120. package/dist/ui/input/mouse.d.ts +12 -2
  121. package/dist/ui/input/mouse.js +20 -7
  122. package/dist/ui/input/references.d.ts +1 -1
  123. package/dist/ui/status/model.d.ts +7 -0
  124. package/dist/ui/status/model.js +5 -0
  125. package/dist/ui/theme/index.d.ts +1 -1
  126. package/package.json +6 -4
  127. package/dist/ui/commands/parse.d.ts +0 -104
  128. package/dist/ui/commands/parse.js +0 -135
  129. package/dist/ui/commands/registry.d.ts +0 -33
  130. package/dist/ui/commands/registry.js +0 -73
@@ -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, ModelState, OptionState, PanelState, 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,17 +1,5 @@
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
1
  import { Transcript } from './transcript.ts';
14
- import type { HistorySearch } from './types.ts';
2
+ import type { AnswerValue, HistorySearch } from './types.ts';
15
3
  import type { ObjectValue } from '../transport/wire.ts';
16
4
  /** A durable user prompt as the transcript reports it, before retention. */
17
5
  export interface PromptRecord {
@@ -31,49 +19,24 @@ export interface PromptLimits {
31
19
  export declare const DEFAULT_PROMPT_LIMITS: PromptLimits;
32
20
  /** Flatten one transcript prompt into the single line the composer recalls. */
33
21
  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
22
  /** Model dialog step: the catalog plus the provider or model being inspected. */
63
23
  export interface ModelState {
64
24
  catalog: ObjectValue;
65
25
  provider?: string;
66
26
  model?: ObjectValue;
67
27
  }
68
- /** Panels the reader opened for the selected session.
28
+ /** Panels the reader opened; visibility and query text only, so the UI owns them.
69
29
  *
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`.
30
+ * One container rather than one flag per call site, so the composition root has a single panel set
31
+ * to gate keys, reset on a session switch and close from a command. Every session panel's rows come
32
+ * from the selected record and its cursor is focus held by `Picker`; the one exception is `prompts`,
33
+ * which lists a client-global file and is therefore not derived from the record.
73
34
  */
74
35
  export interface PanelState {
75
36
  thoughts: boolean;
76
37
  queue: boolean;
38
+ /** Saved shortcut prompts opened by `/prompt`; a client-global list, kept here for one registry. */
39
+ prompts?: boolean;
77
40
  model?: ModelState;
78
41
  history?: {
79
42
  query: string;
@@ -98,7 +61,7 @@ export interface OptionState {
98
61
  * `SessionController.interactions`, which is derived per session on every publish.
99
62
  */
100
63
  export interface InteractionState {
101
- answers: Record<string, ObjectValue[]>;
64
+ answers: Record<string, AnswerValue['answers']>;
102
65
  option?: OptionState;
103
66
  approval?: {
104
67
  eventId: string;
@@ -107,7 +70,7 @@ export interface InteractionState {
107
70
  }
108
71
  /** Client-owned state of the selected session, reset whenever another session is opened.
109
72
  *
110
- * It holds the record, the prompt index, the composer, the reading view and the local interaction
73
+ * It holds the record, the prompt index, the reading view and the local interaction
111
74
  * state because all five belong to one session and none of them is owned by the host beyond what the
112
75
  * record mirrors; everything derivable from `Telemetry` or `CostLedger` stays out (see the design's
113
76
  * §5.7.6). The record is referenced here and nowhere else, so "the selected session" has one entry.
@@ -117,11 +80,9 @@ export declare class SessionInfo {
117
80
  /** The selected session's record. Replaced — never mutated in place — when another session opens. */
118
81
  record: Transcript;
119
82
  readonly prompts: PromptIndex;
120
- readonly composer: ComposerState;
121
- readonly view: ViewState;
83
+ /** Detached history window the reader jumped to; released by `closeWindow` and `reset`. */
84
+ window?: Transcript;
122
85
  readonly interaction: InteractionState;
123
- readonly reference: ReferenceState;
124
- readonly panels: PanelState;
125
86
  constructor(sessionId?: string);
126
87
  /** Forget everything a previous session owned, keeping this instance identity-stable for `State`. */
127
88
  reset(sessionId?: string): void;
@@ -143,6 +104,12 @@ export declare class PromptIndex {
143
104
  private shed;
144
105
  private position;
145
106
  private draft;
107
+ /** Prompts the client sent on the operator's behalf; their durable echo never enters recall.
108
+ *
109
+ * Kept across `reset()` so re-opening the session in the same process stays clean, and bounded
110
+ * because an agent loop can send a prompt per attempt.
111
+ */
112
+ private readonly internal;
146
113
  constructor(limits?: PromptLimits);
147
114
  /** Retained entry count, so a caller can tell an empty index from a parked cursor. */
148
115
  get length(): number;
@@ -167,8 +134,14 @@ export declare class PromptIndex {
167
134
  * live window holds, so the lazy backward step could not recover them.
168
135
  */
169
136
  markComplete(): void;
170
- /** Forget one session's prompts and cursor. */
137
+ /** Forget one session's prompts and cursor; client-generated suppression is process-wide. */
171
138
  reset(): void;
139
+ /** Remember one prompt the client sent itself, so its durable echo never enters recall.
140
+ *
141
+ * An agent loop submits many turns; without this they would crowd out what the operator typed.
142
+ * @param value - Prompt text the client sent on the operator's behalf.
143
+ */
144
+ suppress(value: string): void;
172
145
  /** Fold one transcript scan: its prompts, plus the watermark it covered.
173
146
  *
174
147
  * 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);
@@ -167,7 +180,7 @@ export class PromptIndex {
167
180
  */
168
181
  prepend(values) {
169
182
  const older = values.map(value => ({ seq: value.seq, text: promptText(value.text), durable: true }))
170
- .filter(value => this.retainable(value));
183
+ .filter(value => !this.internal.has(value.text) && this.retainable(value));
171
184
  if (!older.length)
172
185
  return 0;
173
186
  this.entries = [...older, ...this.entries];
@@ -213,7 +226,8 @@ export class PromptIndex {
213
226
  /** Retain one durable prompt; a durable echo upgrades a local entry instead of duplicating it. */
214
227
  push(value) {
215
228
  const entry = { seq: value.seq, text: promptText(value.text), durable: true };
216
- if (!this.retainable(entry))
229
+ // A prompt the client itself sent (an agent loop) is durable history, but not composer recall.
230
+ if (this.internal.has(entry.text) || !this.retainable(entry))
217
231
  return;
218
232
  const last = this.entries.at(-1);
219
233
  if (last && last.text === entry.text) {
@@ -5,7 +5,7 @@ import stringWidth from 'string-width';
5
5
  import wrapAnsi from 'wrap-ansi';
6
6
  import { stripVTControlCharacters } from 'node:util';
7
7
  import { decodeHTML } from 'entities';
8
- import { safeText } from "../transport/wire.js";
8
+ import { safeText } from "../text.js";
9
9
  import { renderMath } from "./math.js";
10
10
  const parser = new Marked({ gfm: true });
11
11
  // Math is tokenized before Markdown escapes, but after fenced and inline code have claimed their source.
@@ -8,7 +8,7 @@ import { TextNode } from '@mathjax/src/js/core/MmlTree/MmlNode.js';
8
8
  import { SerializedMmlVisitor } from '@mathjax/src/js/core/MmlTree/SerializedMmlVisitor.js';
9
9
  import '@mathjax/src/js/input/tex/base/BaseConfiguration.js';
10
10
  import '@mathjax/src/js/input/tex/ams/AmsConfiguration.js';
11
- import { safeText } from "../transport/wire.js";
11
+ import { safeText } from "../text.js";
12
12
  RegisterHTMLHandler(liteAdaptor());
13
13
  const superscripts = Object.fromEntries([...('0123456789+-=()ni')].map((c, i) => [c, [...'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁼⁽⁾ⁿⁱ'][i]]));
14
14
  const subscripts = Object.fromEntries([...'0123456789+-=()aehijklmnoprstuvx'].map((c, i) => [c, [...'₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓ'][i]]));
@@ -0,0 +1,51 @@
1
+ /** One session's mutation admission order, with a control lane that overtakes the normal waiters.
2
+ *
3
+ * The gate serializes the **admission** of a mutation — checking state, deciding, and issuing the
4
+ * request — not the remote mutation itself. A section therefore returns as soon as the request is
5
+ * issued, and the caller awaits the host's answer outside the gate: a `/compact` can take minutes, and
6
+ * holding the gate for it would make `cancel` wait for exactly the operation it exists to interrupt.
7
+ * The `return dispatch()` below is what enforces that: the gate releases when the section *returns*,
8
+ * so even an `async` section holds it only up to its first `await`.
9
+ *
10
+ * Two lanes:
11
+ *
12
+ * * `normal` — a prompt, steering, a host command, an answer, a queue removal: arrival order;
13
+ * * `control` — `cancelTurn`/`interrupt` and `/loop stop`: waits only for the section already running,
14
+ * so it is admitted ahead of every normal admission that is still waiting.
15
+ *
16
+ * The key is the **target** session, not the selected one: cancelling a forked verifier addresses the
17
+ * verifier's own session, and that must not queue behind the reviewed session's writes.
18
+ */
19
+ /** Which queue one mutation admission joins. */
20
+ export type MutationLane = 'normal' | 'control';
21
+ /** One admission, as the gate reports it so the trace can answer "who dispatched first". */
22
+ export interface MutationAdmission {
23
+ /** Session the mutation targets; never inferred from the selection. */
24
+ readonly sessionId: string;
25
+ /** Lane the admission used. */
26
+ readonly lane: MutationLane;
27
+ /** Whether another admission already held this session's gate when this one arrived. */
28
+ readonly waited: boolean;
29
+ }
30
+ /** Serializes one session's mutation admissions. */
31
+ export declare class SessionMutationGate {
32
+ private readonly report?;
33
+ private readonly lanes;
34
+ /** @param report - Optional observer, called once per admission in dispatch order. */
35
+ constructor(report?: ((admission: MutationAdmission) => void) | undefined);
36
+ /** Wait for this session's turn, then run one section inside the gate.
37
+ *
38
+ * `dispatch` must decide and **issue**: return the promise the caller will await, rather than
39
+ * awaiting it here, or the gate stays held for the whole remote mutation.
40
+ * @param sessionId - Target session the mutation belongs to.
41
+ * @param lane - `control` overtakes waiting normal admissions; `normal` keeps arrival order.
42
+ * @param dispatch - Synchronous decision plus request issue.
43
+ * @returns What `dispatch` returned; a returned promise is adopted, so the caller may await the
44
+ * host's answer without holding the gate.
45
+ */
46
+ admit<T>(sessionId: string, lane: MutationLane, dispatch: () => T): Promise<T>;
47
+ /** Take the gate now, or queue for it behind the running section. */
48
+ private acquire;
49
+ /** Hand the gate to the next waiter, control first, or free it. */
50
+ private release;
51
+ }
@@ -0,0 +1,73 @@
1
+ /** One session's mutation admission order, with a control lane that overtakes the normal waiters.
2
+ *
3
+ * The gate serializes the **admission** of a mutation — checking state, deciding, and issuing the
4
+ * request — not the remote mutation itself. A section therefore returns as soon as the request is
5
+ * issued, and the caller awaits the host's answer outside the gate: a `/compact` can take minutes, and
6
+ * holding the gate for it would make `cancel` wait for exactly the operation it exists to interrupt.
7
+ * The `return dispatch()` below is what enforces that: the gate releases when the section *returns*,
8
+ * so even an `async` section holds it only up to its first `await`.
9
+ *
10
+ * Two lanes:
11
+ *
12
+ * * `normal` — a prompt, steering, a host command, an answer, a queue removal: arrival order;
13
+ * * `control` — `cancelTurn`/`interrupt` and `/loop stop`: waits only for the section already running,
14
+ * so it is admitted ahead of every normal admission that is still waiting.
15
+ *
16
+ * The key is the **target** session, not the selected one: cancelling a forked verifier addresses the
17
+ * verifier's own session, and that must not queue behind the reviewed session's writes.
18
+ */
19
+ /** Serializes one session's mutation admissions. */
20
+ export class SessionMutationGate {
21
+ report;
22
+ lanes = new Map();
23
+ /** @param report - Optional observer, called once per admission in dispatch order. */
24
+ constructor(report) {
25
+ this.report = report;
26
+ }
27
+ /** Wait for this session's turn, then run one section inside the gate.
28
+ *
29
+ * `dispatch` must decide and **issue**: return the promise the caller will await, rather than
30
+ * awaiting it here, or the gate stays held for the whole remote mutation.
31
+ * @param sessionId - Target session the mutation belongs to.
32
+ * @param lane - `control` overtakes waiting normal admissions; `normal` keeps arrival order.
33
+ * @param dispatch - Synchronous decision plus request issue.
34
+ * @returns What `dispatch` returned; a returned promise is adopted, so the caller may await the
35
+ * host's answer without holding the gate.
36
+ */
37
+ async admit(sessionId, lane, dispatch) {
38
+ const state = this.lanes.get(sessionId) ?? { busy: false, control: [], normal: [] };
39
+ this.lanes.set(sessionId, state);
40
+ const waited = state.busy;
41
+ await this.acquire(state, lane);
42
+ this.report?.({ sessionId, lane, waited });
43
+ try {
44
+ return dispatch();
45
+ }
46
+ finally {
47
+ this.release(sessionId, state);
48
+ }
49
+ }
50
+ /** Take the gate now, or queue for it behind the running section. */
51
+ async acquire(state, lane) {
52
+ if (!state.busy) {
53
+ state.busy = true;
54
+ return;
55
+ }
56
+ // The waiter that is woken inherits the gate: `busy` stays true across the handoff, so an
57
+ // admission arriving during it queues instead of overtaking the waiter already promised the turn.
58
+ await new Promise(resolve => (lane === 'control' ? state.control : state.normal).push(resolve));
59
+ }
60
+ /** Hand the gate to the next waiter, control first, or free it. */
61
+ release(sessionId, state) {
62
+ const next = state.control.shift() ?? state.normal.shift();
63
+ if (next !== undefined) {
64
+ next();
65
+ return;
66
+ }
67
+ state.busy = false;
68
+ // A lane nobody is waiting for would otherwise stay behind for the life of the process, and long
69
+ // runs create a session per verification.
70
+ if (this.lanes.get(sessionId) === state)
71
+ this.lanes.delete(sessionId);
72
+ }
73
+ }
@@ -1,91 +1,4 @@
1
- /** Shared display names and unambiguous slash-command target resolution. */
2
- import { type ObjectValue } from '../transport/wire.ts';
3
- /** Parse workspace and resume navigation, including their long aliases. */
4
- export declare function navigationCommand(value: string): {
5
- kind: 'workspace' | 'session';
6
- query?: string;
7
- } | undefined;
8
- /** Resolve the host's title projection, falling back to the session ID. */
9
- export declare function sessionLabel(session: ObjectValue): string;
1
+ /** Unambiguous slash-command target resolution and host title projection. */
2
+ import { type ObjectValue } from '../json.ts';
10
3
  /** Match an exact ID or name before a unique ID prefix; never choose an ambiguous target. */
11
4
  export declare function resolveTarget(items: ObjectValue[], query: string, id: string, names: (item: ObjectValue) => string[]): ObjectValue;
12
- /** User-visible activity of one session, most actionable first.
13
- *
14
- * `needs` is the only state that asks the user to do something, and it outranks the host's running
15
- * flag because a turn waiting on an answer is running only in the mechanical sense. `blank` stays a
16
- * marker for a session that never sent a turn, but it is not a status and never enters a rollup.
17
- */
18
- export type SessionState = 'needs' | 'running' | 'idle' | 'blank';
19
- /** Classify one session from the host's list summary; nothing is inferred from silence.
20
- * @param session - Session summary from `session/list`.
21
- * @param pending - Whether this client holds an unanswered interaction for that session.
22
- * @returns Needs-you while an answer is owed, running while its agent works, otherwise idle or blank.
23
- */
24
- export declare function sessionState(session: ObjectValue, pending?: boolean): SessionState;
25
- /** Leading marker per state: a question mark, a working clock, a filled dot, and an unused circle. */
26
- export declare const SESSION_MARKERS: Record<SessionState, string>;
27
- /** One word per state, so every screen names the same state the same way. */
28
- export declare const STATE_LABELS: Record<SessionState, string>;
29
- /** Coarse age of a session's last activity, so the column stays steady between list refreshes.
30
- * @param time - Epoch milliseconds of the last activity, when the summary reported one.
31
- * @param now - Current epoch milliseconds.
32
- * @returns `now`, minutes, hours or days.
33
- */
34
- export declare function activityAge(time: number | undefined, now: number): string;
35
- /** Status cell for one session row: its state marker and the age of its last activity.
36
- * @param session - Session summary from `session/list`.
37
- * @param now - Current epoch milliseconds.
38
- * @param pending - Whether this client holds an unanswered interaction for that session.
39
- * @returns Marker with an optional age, without a trailing space when unknown.
40
- */
41
- export declare function sessionStatus(session: ObjectValue, now: number, pending?: boolean): string;
42
- /** States a workspace rollup reports, most actionable first. */
43
- export declare const ROLLUP_STATES: readonly ["needs", "running", "idle"];
44
- /** One state a workspace rollup reports. */
45
- export type RollupState = (typeof ROLLUP_STATES)[number];
46
- /** One counted state of a workspace rollup. */
47
- export interface RollupCount {
48
- state: RollupState;
49
- count: number;
50
- }
51
- /** How much room a rollup has for words. */
52
- export type RollupStyle = 'words' | 'badges';
53
- /** Count the sessions of one workspace by the state each reports.
54
- *
55
- * Blank sessions are counted by neither a badge nor a word: a session that never sent a turn is the
56
- * absence of activity, and listing it beside real work only makes the rollup harder to read.
57
- * @param sessions - Sessions whose `sessionIds` belong to the workspace.
58
- * @param pending - Session IDs this client holds an unanswered interaction for.
59
- * @returns One count per state that occurs, most actionable first, or an empty list.
60
- */
61
- export declare function workspaceCounts(sessions: readonly ObjectValue[], pending?: ReadonlySet<string>): RollupCount[];
62
- /** Render one rollup as separately coloured cells.
63
- *
64
- * Each cell after the first carries the separator that joins it to the previous one, so a caller can
65
- * colour the cells independently without losing the text {@link workspaceStatus} would produce.
66
- * @param counts - Counts from {@link workspaceCounts}.
67
- * @param style - `words` spells each state out; `badges` keeps only the marker and the count.
68
- * @returns The cells in the order given, with their separators.
69
- */
70
- export declare function workspaceSegments(counts: readonly RollupCount[], style?: RollupStyle): {
71
- state: RollupState;
72
- text: string;
73
- }[];
74
- /** Render one rollup as plain text, the same way every screen and test reads it.
75
- * @param counts - Counts from {@link workspaceCounts}.
76
- * @param style - `words` spells each state out; `badges` keeps only the marker and the count.
77
- * @returns The joined cell text, empty when nothing was counted.
78
- */
79
- export declare function workspaceStatus(counts: readonly RollupCount[], style?: RollupStyle): string;
80
- /** Marker key for the compact rollup, which has no room for the words. */
81
- export declare const ROLLUP_LEGEND: string;
82
- /** Secondary path text for one workspace row.
83
- *
84
- * The title is usually the last path segment, so repeating it wastes the row; the parent directory
85
- * is what distinguishes two checkouts. A title that does not name the last segment keeps the full
86
- * path, because dropping it would hide where the workspace actually lives.
87
- * @param path - Registered host directory.
88
- * @param title - Workspace title as the row already shows it.
89
- * @returns The path to show beside the row, or an empty string when nothing is left.
90
- */
91
- export declare function workspaceDetail(path: string, title: string): string;