@itookit/dsht 0.3.4 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/dist/controller/controller.d.ts +107 -0
- package/dist/controller/controller.js +114 -1
- package/dist/cost/controller.d.ts +5 -0
- package/dist/cost/controller.js +2 -1
- package/dist/cost/scanner.d.ts +4 -2
- package/dist/cost/scanner.js +6 -3
- package/dist/session/controller.d.ts +141 -1
- package/dist/session/controller.js +362 -29
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +1 -0
- package/dist/session/info.d.ts +262 -0
- package/dist/session/info.js +326 -0
- package/dist/session/transcript.d.ts +37 -1
- package/dist/session/transcript.js +73 -0
- package/dist/state.d.ts +3 -2
- package/dist/state.js +2 -2
- package/dist/ui/app.js +183 -173
- package/dist/ui/chat/status.js +4 -4
- package/dist/ui/dialogs/index.d.ts +3 -6
- package/dsht-m.png +0 -0
- package/package.json +1 -1
- package/dist/ui/input/history.d.ts +0 -19
- package/dist/ui/input/history.js +0 -43
package/README.i18n.yaml
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Git blob hashes of the reviewed bilingual pair.
|
|
2
|
-
README.md:
|
|
3
|
-
README.zh.md:
|
|
2
|
+
README.md: 92c2be7222eebf3a4377d320d6107415e483a338
|
|
3
|
+
README.zh.md: 810b6f5f4cc3a417aa99d0c61aa61700d6df3a37
|
package/README.md
CHANGED
|
@@ -297,7 +297,7 @@ In `/ws` and `/resume` pickers, every row reports one of three user-visible stat
|
|
|
297
297
|
|
|
298
298
|
`/search` matches literal text case-insensitively in conversation messages, including older pages; tool-only rows are excluded. Search scans up to 80 messages per request, discards each temporary page, and retains at most 200 short matches, including folded reasoning. A truncated result asks you to refine the query. Opening a match loads a separate page around its sequence; `/latest` releases that window. Esc or Ctrl+C cancels the search. A rare or absent term still requires scanning the full history over HTTP; there is no server-side full-text index for this command. `/history` lists your own prompts from the loaded pages. Record sequences are the numbers shown by these pickers. `/ssearch` and `/wsearch` call `session/search`, which searches current user/assistant message content and returns at most 20 sessions, snippets, and a truncation flag; it exposes neither a result cursor nor matching record sequences. Workspace filtering happens after that global limit, so a truncated workspace result can omit matches. The UI warns when results are incomplete; refine the query. Selecting a session loads its history and offers matching messages for the jump. These operations use HTTP and never scan the host configuration directory.
|
|
299
299
|
|
|
300
|
-
↑/↓ or Ctrl+P/N recalls previously submitted prompts and slash commands without sending them; Enter submits the recalled text. A reading panel that fits the screen leaves the arrows with this history, a panel that has to scroll takes them for itself, and Ctrl+P/N reach the history from any panel. Moving past the newest entry restores the unsent draft. Editing recalled text starts a new draft. Recall keeps up to
|
|
300
|
+
↑/↓ or Ctrl+P/N recalls previously submitted prompts and slash commands without sending them; Enter submits the recalled text. A reading panel that fits the screen leaves the arrows with this history, a panel that has to scroll takes them for itself, and Ctrl+P/N reach the history from any panel. Moving past the newest entry restores the unsent draft. Editing recalled text starts a new draft, and switching sessions clears an unsent one. Recall keeps the selected session's user prompts — up to 2,000 entries and approximately 512 KiB of text — and never writes a separate history file; switching sessions releases them. It folds those prompts from the session records as they arrive, so it covers prompts from before this client connected rather than only the window that happened to load. Opening a session also walks the earlier history in the background, keeping only prompts rather than loading those pages into the conversation, so the whole session's prompt list is available from the start. Reaching its oldest retained prompt first refills from the already-loaded conversation at no request cost, and only then fetches the page before the window, inside one bounded loop that skips tool-only pages so the key cannot stall; the fetched page also stays in the conversation above the composer. Eviction therefore bounds memory without deciding reachability: an evicted prompt is either still loaded or still on the host. Consecutive duplicates are merged, oversized entries are skipped, and question or approval answers are excluded. Question options and completion menus keep arrow navigation; workspace/session lists use arrows when the composer is empty, with Ctrl+P/N available for recall.
|
|
301
301
|
|
|
302
302
|
Within each User group, only the first assistant prose or reasoning message shows an Assistant heading. Later messages and live output reuse that heading across tool results and Context messages. A newly loaded history window starts its own visible group; message sequences, tool status, search, and reasoning expansion remain independent.
|
|
303
303
|
|
|
@@ -426,7 +426,7 @@ This repository publishes one public package, `@itookit/dsht`, from the `mushuan
|
|
|
426
426
|
|
|
427
427
|
| Field | Value |
|
|
428
428
|
| --- | --- |
|
|
429
|
-
| Name and version | `@itookit/dsht` `0.3.
|
|
429
|
+
| Name and version | `@itookit/dsht` `0.3.7` |
|
|
430
430
|
| Executable | `dsht`, or `npx @itookit/dsht` without installing |
|
|
431
431
|
| Library entries | `@itookit/dsht` and `@itookit/dsht/auth` |
|
|
432
432
|
| Author | lizlok@gmail.com |
|
package/README.zh.md
CHANGED
|
@@ -297,7 +297,7 @@ Enter 提交消息:代理处于 Working 时自动作为下一个 step 的转
|
|
|
297
297
|
|
|
298
298
|
`/search` 对对话消息进行不区分大小写的字面文本匹配,包含旧页,排除纯工具行。搜索每次请求最多 80 条消息,扫描后释放临时页,只保留最多 200 条简短命中摘要,包含折叠的思考;结果截断时提示缩小查询范围。选择命中项只加载其序号附近的独立页面,`/latest` 释放该窗口。Esc 或 Ctrl+C 可取消搜索。稀有词或无匹配查询仍需通过 HTTP 扫描全部历史,此命令尚无服务端全文索引。`/history` 只列出已加载页面中自己的提示词,选择器显示的数字就是记录序号。`/ssearch` 与 `/wsearch` 调用 `session/search`,服务端搜索当前用户/助手消息内容,最多返回 20 个会话、摘要和截断标记,没有结果分页游标或命中记录序号。工作区筛选在全局数量限制之后进行,因此截断时可能漏掉工作区内的匹配会话;界面会提示结果不完整,可缩小查询范围。选择会话后加载其历史,再选择匹配消息跳转。所有操作均通过 HTTP 完成,不扫描服务端配置目录。
|
|
299
299
|
|
|
300
|
-
↑/↓ 或 Ctrl+P/N 回填之前提交的提示词和 slash 命令,按 Enter 才提交。一屏放得下的阅读面板会把方向键留给该历史,需要滚动的面板才接管方向键,而 Ctrl+P/N
|
|
300
|
+
↑/↓ 或 Ctrl+P/N 回填之前提交的提示词和 slash 命令,按 Enter 才提交。一屏放得下的阅读面板会把方向键留给该历史,需要滚动的面板才接管方向键,而 Ctrl+P/N 在任何面板打开时都能回填。向下越过最新记录时恢复未发送草稿;编辑回填内容后开始新的草稿,切换会话会清空未发送的草稿。回填按当前会话保留其 user prompt,最多 2,000 条、约 512 KiB 文本,且不写入独立历史文件;切换会话即释放。它在记录到达时增量折叠这些提示词,因此覆盖本客户端连接之前的提示词,而不只是碰巧加载的那个窗口。打开会话后它还会在后台把更早的历史翻一遍,只提取提示词、不把这些页读进上方对话,因此从会话开始就持有整个会话的提示词列表。走到索引里最旧一条时,先用已加载的对话补回被预算淘汰的提示词,这一步不发请求;只有窗口也用尽才取回窗口之前的一页,并在同一次有界循环里跳过整页没有 User 消息的页,因此按键不会被工具页卡住;取回的那页也会留在输入框上方的对话里。于是淘汰只约束内存、不决定可达性:被淘汰的提示词要么仍在窗口内,要么仍在宿主上。连续重复输入合并,超大输入跳过,提问和审批回答不记入历史。提问选项与补全菜单优先使用箭头;工作区/会话列表在输入框为空时使用箭头选择,可用 Ctrl+P/N 调出输入历史。
|
|
301
301
|
|
|
302
302
|
每条 User 消息之后,只在第一段助手正文或思考前显示 Assistant 标题;后续消息及流式输出沿用分组,工具结果和 Context 消息不重置分组。当前加载的历史窗口从自身起点建立可见分组,消息序号、工具状态、搜索和思考展开仍各自保留。
|
|
303
303
|
|
|
@@ -426,7 +426,7 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
426
426
|
|
|
427
427
|
| 字段 | 值 |
|
|
428
428
|
| --- | --- |
|
|
429
|
-
| 名称与版本 | `@itookit/dsht` `0.3.
|
|
429
|
+
| 名称与版本 | `@itookit/dsht` `0.3.7` |
|
|
430
430
|
| 可执行命令 | `dsht`,不安装时用 `npx @itookit/dsht` |
|
|
431
431
|
| 库入口 | `@itookit/dsht` 和 `@itookit/dsht/auth` |
|
|
432
432
|
| 作者 | lizlok\@gmail.com |
|
|
@@ -12,6 +12,8 @@ import { ConnectionController, type ConnectionListener } from './connection.ts';
|
|
|
12
12
|
import { MemoryLog } from './memory-log.ts';
|
|
13
13
|
import { type ControllerStore, type State } from '../state.ts';
|
|
14
14
|
import type { HistorySearch, RemovalTarget } from '../session/types.ts';
|
|
15
|
+
import type { ComposerState, InteractionState, ModelState, OptionState, PanelState, ReferenceState, ViewState } from '../session/info.ts';
|
|
16
|
+
import type { Reasoning } from '../session/history.ts';
|
|
15
17
|
/** Application facade over the domain controllers; the UI owns only this object.
|
|
16
18
|
*
|
|
17
19
|
* State lives here, connection generations live in `connection`, the selected session and its
|
|
@@ -47,6 +49,8 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
47
49
|
snapshot: () => State;
|
|
48
50
|
/** Current projection store; replaced at each connection generation. */
|
|
49
51
|
get telemetry(): Telemetry;
|
|
52
|
+
/** Record of the selected session; the one strong owner lives in `State.session`. */
|
|
53
|
+
get record(): Transcript;
|
|
50
54
|
/** @returns The current selector generation. */
|
|
51
55
|
selection(): number;
|
|
52
56
|
/** Advance the selector generation when the selected workspace or session changes. */
|
|
@@ -141,10 +145,113 @@ export declare class Controller implements ControllerStore, ConnectionListener {
|
|
|
141
145
|
* @returns True when the caller may exit.
|
|
142
146
|
*/
|
|
143
147
|
interrupt(force?: boolean): Promise<boolean>;
|
|
148
|
+
/** One-line estimate of the selected session's cost, or `?` while the ledger has no entry for it. */
|
|
149
|
+
get sessionCostText(): string;
|
|
144
150
|
/** Keep history stable while the user reads, searches, or expands it.
|
|
145
151
|
* @param pinned - Whether the main transcript is being read away from its tail.
|
|
146
152
|
*/
|
|
147
153
|
pinHistory(pinned: boolean): void;
|
|
154
|
+
/** Recall one step through the selected session's prompt index; never touches the network.
|
|
155
|
+
* @param direction - Negative for older input, positive for newer input.
|
|
156
|
+
* @param current - Composer content before recall began, restored at the newest position.
|
|
157
|
+
* @returns The recalled prompt, or the unsent draft.
|
|
158
|
+
*/
|
|
159
|
+
recall(direction: -1 | 1, current: string): string;
|
|
160
|
+
/** Remember a locally submitted command, which never becomes a durable session record.
|
|
161
|
+
* @param value - Submitted command text.
|
|
162
|
+
*/
|
|
163
|
+
recordRecall(value: string): void;
|
|
164
|
+
/** Leave recall navigation because the composer was edited or replaced. */
|
|
165
|
+
resetRecall(): void;
|
|
166
|
+
/** Whether recall is parked on the oldest prompt the session retains. */
|
|
167
|
+
get recallAtOldest(): boolean;
|
|
168
|
+
/** How many prompts the selected session retains for recall. */
|
|
169
|
+
get recallLength(): number;
|
|
170
|
+
/** Whether an older prompt is reachable, in the loaded window or on the host. */
|
|
171
|
+
get recallHasOlder(): boolean;
|
|
172
|
+
/** Recover older prompts from the loaded window before spending a page request.
|
|
173
|
+
* @returns Whether any older prompt was recovered.
|
|
174
|
+
*/
|
|
175
|
+
refillRecall(): boolean;
|
|
176
|
+
/** Composer draft, caret and parked draft of the selected session. */
|
|
177
|
+
get composer(): ComposerState;
|
|
178
|
+
/** Replace the composer text and caret.
|
|
179
|
+
* @param draft - New text.
|
|
180
|
+
* @param cursor - Caret column; defaults to the end of the text.
|
|
181
|
+
*/
|
|
182
|
+
setComposer(draft: string, cursor?: number): void;
|
|
183
|
+
/** Move the caret without changing the text.
|
|
184
|
+
* @param cursor - Caret column.
|
|
185
|
+
*/
|
|
186
|
+
setComposerCursor(cursor: number): void;
|
|
187
|
+
/** Move a non-empty draft aside while a dialog owns the keyboard. */
|
|
188
|
+
parkComposer(): void;
|
|
189
|
+
/** Give a parked draft back once no dialog needs the keyboard. */
|
|
190
|
+
restoreComposer(): void;
|
|
191
|
+
/** How the selected session's record is being read right now. */
|
|
192
|
+
get view(): ViewState;
|
|
193
|
+
/** Show a detached history window, releasing the one it replaces.
|
|
194
|
+
* @param window - Record to display, or undefined to return to the live transcript.
|
|
195
|
+
*/
|
|
196
|
+
setViewWindow(window?: Transcript): void;
|
|
197
|
+
/** Move the reader's position inside the displayed record.
|
|
198
|
+
* @param scroll - Rows scrolled back from the live end.
|
|
199
|
+
*/
|
|
200
|
+
setScroll(scroll: number): void;
|
|
201
|
+
/** Replace the set of expanded reasoning blocks.
|
|
202
|
+
* @param folds - Sequences to expand beyond the default fold.
|
|
203
|
+
*/
|
|
204
|
+
setFolds(folds: ReadonlySet<number>): void;
|
|
205
|
+
/** Set the fold mode of the live attempt's completed reasoning.
|
|
206
|
+
* @param reasoning - `row` to fold, `full` to keep the streamed text.
|
|
207
|
+
*/
|
|
208
|
+
setLiveReasoning(reasoning: Reasoning): void;
|
|
209
|
+
/** Local answer state for the selected session's pending waterfalls. */
|
|
210
|
+
get interaction(): InteractionState;
|
|
211
|
+
/** Replace the partly collected answers, keyed by waterfall event id.
|
|
212
|
+
* @param answers - Answers collected so far, by event id.
|
|
213
|
+
*/
|
|
214
|
+
setAnswers(answers: Record<string, ObjectValue[]>): void;
|
|
215
|
+
/** Replace the pending question's option keyboard state.
|
|
216
|
+
* @param option - Highlighted option, toggled labels and free-text mode; undefined clears it.
|
|
217
|
+
*/
|
|
218
|
+
setOption(option?: OptionState): void;
|
|
219
|
+
/** Replace the pending approval's selected row.
|
|
220
|
+
* @param approval - Selected approval row; undefined clears the highlight.
|
|
221
|
+
*/
|
|
222
|
+
setApproval(approval?: InteractionState['approval']): void;
|
|
223
|
+
/** Composer-adjacent `@` reference menu state. */
|
|
224
|
+
get reference(): ReferenceState;
|
|
225
|
+
/** Highlight one row of the open reference menu.
|
|
226
|
+
* @param index - Row index into the current matches.
|
|
227
|
+
*/
|
|
228
|
+
setReferenceIndex(index: number): void;
|
|
229
|
+
/** Remember the draft that dismissed the reference menu.
|
|
230
|
+
* @param draft - Composer text at dismissal, or undefined to allow the menu again.
|
|
231
|
+
*/
|
|
232
|
+
setReferenceDismissed(draft?: string): void;
|
|
233
|
+
/** Panels the selected session has open. */
|
|
234
|
+
get panels(): PanelState;
|
|
235
|
+
/** Show or hide the reasoning panel.
|
|
236
|
+
* @param open - Whether `/think` is open.
|
|
237
|
+
*/
|
|
238
|
+
openThoughts(open: boolean): void;
|
|
239
|
+
/** Show or hide the pending-input panel.
|
|
240
|
+
* @param open - Whether `/queue` is open.
|
|
241
|
+
*/
|
|
242
|
+
openQueue(open: boolean): void;
|
|
243
|
+
/** Show the model dialog at one step, or close it.
|
|
244
|
+
* @param model - Catalog plus the provider or model being inspected; undefined closes the dialog.
|
|
245
|
+
*/
|
|
246
|
+
setModelPanel(model?: ModelState): void;
|
|
247
|
+
/** Show the history or content-search dialog, or close it.
|
|
248
|
+
* @param history - Query, content-search mode and matches; undefined closes the dialog.
|
|
249
|
+
*/
|
|
250
|
+
setHistoryPanel(history?: PanelState['history']): void;
|
|
251
|
+
/** Show the host session-search results, or close them.
|
|
252
|
+
* @param search - Query, results and truncation flag; undefined closes the dialog.
|
|
253
|
+
*/
|
|
254
|
+
setSearchPanel(search?: PanelState['search']): void;
|
|
148
255
|
/** Refresh all HTTP-visible sessions without changing the selected conversation.
|
|
149
256
|
* @param signal - Optional cancellation for an explicit /cost refresh.
|
|
150
257
|
*/
|
|
@@ -8,6 +8,7 @@ import { markdownCacheStats } from "../session/markdown.js";
|
|
|
8
8
|
import { SessionController } from "../session/controller.js";
|
|
9
9
|
import { CatalogController } from "../catalog/controller.js";
|
|
10
10
|
import { CostController } from "../cost/controller.js";
|
|
11
|
+
import { costText } from "../cost/ledger.js";
|
|
11
12
|
import { ConnectionController } from "./connection.js";
|
|
12
13
|
import { MemoryLog } from "./memory-log.js";
|
|
13
14
|
import { clearReactMeasures, measureCount } from "./perf-measures.js";
|
|
@@ -56,6 +57,10 @@ export class Controller {
|
|
|
56
57
|
online: () => this.state.online,
|
|
57
58
|
signal: () => this.connection.signal(),
|
|
58
59
|
publish: () => this.update({}),
|
|
60
|
+
// The scan already reads every session's whole history; handing its pages to the session
|
|
61
|
+
// domain lets the prompt cache pick them up, so one open does not pay for a second walk.
|
|
62
|
+
scanPage: (sessionId, records) => this.session.rememberScanPage(sessionId, records),
|
|
63
|
+
scanDone: sessionId => this.session.rememberScanDone(sessionId),
|
|
59
64
|
});
|
|
60
65
|
if (memoryLogPath !== undefined)
|
|
61
66
|
this.memoryLog = new MemoryLog(memoryLogPath, () => this.memorySample());
|
|
@@ -69,6 +74,8 @@ export class Controller {
|
|
|
69
74
|
snapshot = () => this.state;
|
|
70
75
|
/** Current projection store; replaced at each connection generation. */
|
|
71
76
|
get telemetry() { return this.connection.telemetry; }
|
|
77
|
+
/** Record of the selected session; the one strong owner lives in `State.session`. */
|
|
78
|
+
get record() { return this.state.session.record; }
|
|
72
79
|
/** @returns The current selector generation. */
|
|
73
80
|
selection() { return this.selector; }
|
|
74
81
|
/** Advance the selector generation when the selected workspace or session changes. */
|
|
@@ -137,7 +144,7 @@ export class Controller {
|
|
|
137
144
|
*/
|
|
138
145
|
memorySample() {
|
|
139
146
|
const memory = process.memoryUsage();
|
|
140
|
-
const transcript = this.state.
|
|
147
|
+
const transcript = this.state.session.record;
|
|
141
148
|
const ledger = this.costs?.summary();
|
|
142
149
|
const layout = layoutStats(transcript);
|
|
143
150
|
const markdown = markdownCacheStats();
|
|
@@ -251,10 +258,116 @@ export class Controller {
|
|
|
251
258
|
* @returns True when the caller may exit.
|
|
252
259
|
*/
|
|
253
260
|
interrupt(force = false) { return this.session.interrupt(force); }
|
|
261
|
+
/** One-line estimate of the selected session's cost, or `?` while the ledger has no entry for it. */
|
|
262
|
+
get sessionCostText() {
|
|
263
|
+
const sessionId = this.state.sessionId;
|
|
264
|
+
return this.costs?.hasSession(sessionId) ? costText(this.costs.total(sessionId)) : '?';
|
|
265
|
+
}
|
|
254
266
|
/** Keep history stable while the user reads, searches, or expands it.
|
|
255
267
|
* @param pinned - Whether the main transcript is being read away from its tail.
|
|
256
268
|
*/
|
|
257
269
|
pinHistory(pinned) { this.session.pinHistory(pinned); }
|
|
270
|
+
/** Recall one step through the selected session's prompt index; never touches the network.
|
|
271
|
+
* @param direction - Negative for older input, positive for newer input.
|
|
272
|
+
* @param current - Composer content before recall began, restored at the newest position.
|
|
273
|
+
* @returns The recalled prompt, or the unsent draft.
|
|
274
|
+
*/
|
|
275
|
+
recall(direction, current) { return this.session.recall(direction, current); }
|
|
276
|
+
/** Remember a locally submitted command, which never becomes a durable session record.
|
|
277
|
+
* @param value - Submitted command text.
|
|
278
|
+
*/
|
|
279
|
+
recordRecall(value) { this.session.recordRecall(value); }
|
|
280
|
+
/** Leave recall navigation because the composer was edited or replaced. */
|
|
281
|
+
resetRecall() { this.session.resetRecall(); }
|
|
282
|
+
/** Whether recall is parked on the oldest prompt the session retains. */
|
|
283
|
+
get recallAtOldest() { return this.session.recallAtOldest; }
|
|
284
|
+
/** How many prompts the selected session retains for recall. */
|
|
285
|
+
get recallLength() { return this.session.recallLength; }
|
|
286
|
+
/** Whether an older prompt is reachable, in the loaded window or on the host. */
|
|
287
|
+
get recallHasOlder() { return this.session.recallHasOlder; }
|
|
288
|
+
/** Recover older prompts from the loaded window before spending a page request.
|
|
289
|
+
* @returns Whether any older prompt was recovered.
|
|
290
|
+
*/
|
|
291
|
+
refillRecall() { return this.session.refillRecall(); }
|
|
292
|
+
/** Composer draft, caret and parked draft of the selected session. */
|
|
293
|
+
get composer() { return this.session.composer; }
|
|
294
|
+
/** Replace the composer text and caret.
|
|
295
|
+
* @param draft - New text.
|
|
296
|
+
* @param cursor - Caret column; defaults to the end of the text.
|
|
297
|
+
*/
|
|
298
|
+
setComposer(draft, cursor) { this.session.setComposer(draft, cursor); }
|
|
299
|
+
/** Move the caret without changing the text.
|
|
300
|
+
* @param cursor - Caret column.
|
|
301
|
+
*/
|
|
302
|
+
setComposerCursor(cursor) { this.session.setComposerCursor(cursor); }
|
|
303
|
+
/** Move a non-empty draft aside while a dialog owns the keyboard. */
|
|
304
|
+
parkComposer() { this.session.parkComposer(); }
|
|
305
|
+
/** Give a parked draft back once no dialog needs the keyboard. */
|
|
306
|
+
restoreComposer() { this.session.restoreComposer(); }
|
|
307
|
+
/** How the selected session's record is being read right now. */
|
|
308
|
+
get view() { return this.session.view; }
|
|
309
|
+
/** Show a detached history window, releasing the one it replaces.
|
|
310
|
+
* @param window - Record to display, or undefined to return to the live transcript.
|
|
311
|
+
*/
|
|
312
|
+
setViewWindow(window) { this.session.setViewWindow(window); }
|
|
313
|
+
/** Move the reader's position inside the displayed record.
|
|
314
|
+
* @param scroll - Rows scrolled back from the live end.
|
|
315
|
+
*/
|
|
316
|
+
setScroll(scroll) { this.session.setScroll(scroll); }
|
|
317
|
+
/** Replace the set of expanded reasoning blocks.
|
|
318
|
+
* @param folds - Sequences to expand beyond the default fold.
|
|
319
|
+
*/
|
|
320
|
+
setFolds(folds) { this.session.setFolds(folds); }
|
|
321
|
+
/** Set the fold mode of the live attempt's completed reasoning.
|
|
322
|
+
* @param reasoning - `row` to fold, `full` to keep the streamed text.
|
|
323
|
+
*/
|
|
324
|
+
setLiveReasoning(reasoning) { this.session.setLiveReasoning(reasoning); }
|
|
325
|
+
/** Local answer state for the selected session's pending waterfalls. */
|
|
326
|
+
get interaction() { return this.session.interaction; }
|
|
327
|
+
/** Replace the partly collected answers, keyed by waterfall event id.
|
|
328
|
+
* @param answers - Answers collected so far, by event id.
|
|
329
|
+
*/
|
|
330
|
+
setAnswers(answers) { this.session.setAnswers(answers); }
|
|
331
|
+
/** Replace the pending question's option keyboard state.
|
|
332
|
+
* @param option - Highlighted option, toggled labels and free-text mode; undefined clears it.
|
|
333
|
+
*/
|
|
334
|
+
setOption(option) { this.session.setOption(option); }
|
|
335
|
+
/** Replace the pending approval's selected row.
|
|
336
|
+
* @param approval - Selected approval row; undefined clears the highlight.
|
|
337
|
+
*/
|
|
338
|
+
setApproval(approval) { this.session.setApproval(approval); }
|
|
339
|
+
/** Composer-adjacent `@` reference menu state. */
|
|
340
|
+
get reference() { return this.session.reference; }
|
|
341
|
+
/** Highlight one row of the open reference menu.
|
|
342
|
+
* @param index - Row index into the current matches.
|
|
343
|
+
*/
|
|
344
|
+
setReferenceIndex(index) { this.session.setReferenceIndex(index); }
|
|
345
|
+
/** Remember the draft that dismissed the reference menu.
|
|
346
|
+
* @param draft - Composer text at dismissal, or undefined to allow the menu again.
|
|
347
|
+
*/
|
|
348
|
+
setReferenceDismissed(draft) { this.session.setReferenceDismissed(draft); }
|
|
349
|
+
/** Panels the selected session has open. */
|
|
350
|
+
get panels() { return this.session.panels; }
|
|
351
|
+
/** Show or hide the reasoning panel.
|
|
352
|
+
* @param open - Whether `/think` is open.
|
|
353
|
+
*/
|
|
354
|
+
openThoughts(open) { this.session.openThoughts(open); }
|
|
355
|
+
/** Show or hide the pending-input panel.
|
|
356
|
+
* @param open - Whether `/queue` is open.
|
|
357
|
+
*/
|
|
358
|
+
openQueue(open) { this.session.openQueue(open); }
|
|
359
|
+
/** Show the model dialog at one step, or close it.
|
|
360
|
+
* @param model - Catalog plus the provider or model being inspected; undefined closes the dialog.
|
|
361
|
+
*/
|
|
362
|
+
setModelPanel(model) { this.session.setModelPanel(model); }
|
|
363
|
+
/** Show the history or content-search dialog, or close it.
|
|
364
|
+
* @param history - Query, content-search mode and matches; undefined closes the dialog.
|
|
365
|
+
*/
|
|
366
|
+
setHistoryPanel(history) { this.session.setHistoryPanel(history); }
|
|
367
|
+
/** Show the host session-search results, or close them.
|
|
368
|
+
* @param search - Query, results and truncation flag; undefined closes the dialog.
|
|
369
|
+
*/
|
|
370
|
+
setSearchPanel(search) { this.session.setSearchPanel(search); }
|
|
258
371
|
/** Refresh all HTTP-visible sessions without changing the selected conversation.
|
|
259
372
|
* @param signal - Optional cancellation for an explicit /cost refresh.
|
|
260
373
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Periodic billing scans; the ledger outlives any single connection generation. */
|
|
2
2
|
import type { Client } from '../transport/client.ts';
|
|
3
|
+
import { type Json } from '../transport/wire.ts';
|
|
3
4
|
import type { CostLedger } from './ledger.ts';
|
|
4
5
|
/** Host access a scan needs; supplied by the controller facade. */
|
|
5
6
|
export interface CostHost {
|
|
@@ -11,6 +12,10 @@ export interface CostHost {
|
|
|
11
12
|
signal(): AbortSignal;
|
|
12
13
|
/** Re-publish controller state after the ledger changes. */
|
|
13
14
|
publish(): void;
|
|
15
|
+
/** Hand one already-read history page to another consumer, which owns what it does with it. */
|
|
16
|
+
scanPage?(sessionId: string, records: readonly Json[]): void;
|
|
17
|
+
/** Report that a session's history was read to its beginning. */
|
|
18
|
+
scanDone?(sessionId: string): void;
|
|
14
19
|
}
|
|
15
20
|
/** Owns the background scan: startup, the minute timer, turn-completion refresh, and `/cost`. */
|
|
16
21
|
export declare class CostController {
|
package/dist/cost/controller.js
CHANGED
|
@@ -77,7 +77,8 @@ export class CostController {
|
|
|
77
77
|
if (!session.running && typeof session.updatedAt === 'number' && this.updates.get(sessionId) === session.updatedAt)
|
|
78
78
|
continue;
|
|
79
79
|
try {
|
|
80
|
-
const history = await sessionCostHistory(client, session, combined, () => { pages++; });
|
|
80
|
+
const history = await sessionCostHistory(client, session, combined, () => { pages++; }, records => this.host.scanPage?.(sessionId, records));
|
|
81
|
+
this.host.scanDone?.(sessionId);
|
|
81
82
|
scanned++;
|
|
82
83
|
events += history.events.length;
|
|
83
84
|
await ledger.replace(sessionId, history.cursor, history.events);
|
package/dist/cost/scanner.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Address and page one session's complete billing history over the host connection. */
|
|
2
2
|
import { type Client } from '../transport/client.ts';
|
|
3
|
-
import { type ObjectValue } from '../transport/wire.ts';
|
|
3
|
+
import { type Json, type ObjectValue } from '../transport/wire.ts';
|
|
4
4
|
/** Wire addresses for one `session/list` row, in the order the cost scan should try them.
|
|
5
5
|
*
|
|
6
6
|
* A subagent child is reachable only under its durable parent, and the list row omits the delivery
|
|
@@ -14,9 +14,11 @@ export declare function costAddresses(session: ObjectValue): ObjectValue[];
|
|
|
14
14
|
* @param session - One row from the host session list.
|
|
15
15
|
* @param signal - Cancels paging without cancelling any agent work.
|
|
16
16
|
* @param onPage - Counts each history request, so a scan can report how much it re-read.
|
|
17
|
+
* @param onRecords - Hands each page's raw records to the caller, which owns any further reading of
|
|
18
|
+
* them; the billing fold itself keeps only the minimal events below.
|
|
17
19
|
* @returns Opening cursor and the minimal billing events behind it.
|
|
18
20
|
*/
|
|
19
|
-
export declare function sessionCostHistory(client: Client, session: ObjectValue, signal: AbortSignal, onPage?: () => void): Promise<{
|
|
21
|
+
export declare function sessionCostHistory(client: Client, session: ObjectValue, signal: AbortSignal, onPage?: () => void, onRecords?: (records: readonly Json[]) => void): Promise<{
|
|
20
22
|
cursor: number;
|
|
21
23
|
events: ObjectValue[];
|
|
22
24
|
}>;
|
package/dist/cost/scanner.js
CHANGED
|
@@ -24,13 +24,15 @@ export function costAddresses(session) {
|
|
|
24
24
|
* @param session - One row from the host session list.
|
|
25
25
|
* @param signal - Cancels paging without cancelling any agent work.
|
|
26
26
|
* @param onPage - Counts each history request, so a scan can report how much it re-read.
|
|
27
|
+
* @param onRecords - Hands each page's raw records to the caller, which owns any further reading of
|
|
28
|
+
* them; the billing fold itself keeps only the minimal events below.
|
|
27
29
|
* @returns Opening cursor and the minimal billing events behind it.
|
|
28
30
|
*/
|
|
29
|
-
export async function sessionCostHistory(client, session, signal, onPage) {
|
|
31
|
+
export async function sessionCostHistory(client, session, signal, onPage, onRecords) {
|
|
30
32
|
let lastError;
|
|
31
33
|
for (const address of costAddresses(session)) {
|
|
32
34
|
try {
|
|
33
|
-
return await readCostHistory(client, address, signal, onPage);
|
|
35
|
+
return await readCostHistory(client, address, signal, onPage, onRecords);
|
|
34
36
|
}
|
|
35
37
|
catch (error) {
|
|
36
38
|
lastError = error;
|
|
@@ -42,7 +44,7 @@ export async function sessionCostHistory(client, session, signal, onPage) {
|
|
|
42
44
|
throw lastError;
|
|
43
45
|
}
|
|
44
46
|
/** Page one addressed session's history into the billing events the ledger folds. */
|
|
45
|
-
async function readCostHistory(client, address, signal, onPage) {
|
|
47
|
+
async function readCostHistory(client, address, signal, onPage, onRecords) {
|
|
46
48
|
onPage?.();
|
|
47
49
|
const snapshot = await new Promise((resolve, reject) => {
|
|
48
50
|
let sub;
|
|
@@ -77,6 +79,7 @@ async function readCostHistory(client, address, signal, onPage) {
|
|
|
77
79
|
while (true) {
|
|
78
80
|
signal.throwIfAborted();
|
|
79
81
|
const records = array(page.records);
|
|
82
|
+
onRecords?.(records);
|
|
80
83
|
events.push(...costRecords(records));
|
|
81
84
|
if (!page.hasMore)
|
|
82
85
|
break;
|
|
@@ -3,6 +3,8 @@ import { type Json, type ObjectValue } from '../transport/wire.ts';
|
|
|
3
3
|
import type { ControllerStore, State } from '../state.ts';
|
|
4
4
|
import type { HistoryLimits } from './memory.ts';
|
|
5
5
|
import { type FileReference } from './references.ts';
|
|
6
|
+
import { type ComposerState, type InteractionState, type ModelState, type OptionState, type PanelState, type ReferenceState, type ViewState } from './info.ts';
|
|
7
|
+
import type { Reasoning } from './history.ts';
|
|
6
8
|
import { Transcript } from './transcript.ts';
|
|
7
9
|
import type { ConnectionView } from './connection-view.ts';
|
|
8
10
|
import type { HistorySearch, RemovalTarget } from './types.ts';
|
|
@@ -14,7 +16,13 @@ export declare class SessionController {
|
|
|
14
16
|
private readonly historyLimits;
|
|
15
17
|
private follow;
|
|
16
18
|
private interactions;
|
|
17
|
-
|
|
19
|
+
/** Cancels the background prompt backfill of the previous selection. */
|
|
20
|
+
private promptBackfill?;
|
|
21
|
+
/** Prompts of sessions this process has already read, so re-opening one costs no page request. */
|
|
22
|
+
private readonly promptCache;
|
|
23
|
+
/** Prompt index, composer and reading view of the selected session; the instance `State.session` exposes. */
|
|
24
|
+
private get info();
|
|
25
|
+
private get prompts();
|
|
18
26
|
private stoppingSession?;
|
|
19
27
|
private interruptTask;
|
|
20
28
|
private admission;
|
|
@@ -185,10 +193,142 @@ export declare class SessionController {
|
|
|
185
193
|
*/
|
|
186
194
|
dismissQuestion(): Promise<void>;
|
|
187
195
|
/** Add a page before the retained window using its fixed opening cut.
|
|
196
|
+
*
|
|
197
|
+
* Reclamation is deliberately not pinned here. Every caller that needs the fetched page to
|
|
198
|
+
* survive is already covered by the view: scrolling back sets a scroll position, `/think` opens a
|
|
199
|
+
* panel, and a recall page folds its prompts into the index before this resolves. A pin set here
|
|
200
|
+
* had no clearing edge, so after one recall page the history budget silently stopped applying for
|
|
201
|
+
* the rest of the session while every following page kept adding records.
|
|
188
202
|
* @param signal - Cancels local paging without interrupting the remote agent.
|
|
189
203
|
* @param transcript - Transcript to extend; defaults to the live one.
|
|
190
204
|
*/
|
|
191
205
|
older(signal?: AbortSignal, transcript?: Transcript): Promise<void>;
|
|
206
|
+
/** Recall one step through the session's prompt index; navigation never touches the network.
|
|
207
|
+
* @param direction - Negative for older input, positive for newer input.
|
|
208
|
+
* @param current - Composer content before recall began, restored at the newest position.
|
|
209
|
+
* @returns The recalled prompt, or the unsent draft.
|
|
210
|
+
*/
|
|
211
|
+
recall(direction: -1 | 1, current: string): string;
|
|
212
|
+
/** Composer draft, caret and parked draft, as the selected session holds them. */
|
|
213
|
+
get composer(): ComposerState;
|
|
214
|
+
/** Replace the composer text and caret, publishing only when either actually changed. */
|
|
215
|
+
setComposer(draft: string, cursor?: number): void;
|
|
216
|
+
/** Move the caret without changing the text. */
|
|
217
|
+
setComposerCursor(cursor: number): void;
|
|
218
|
+
/** Move a non-empty draft aside while a dialog owns the keyboard. */
|
|
219
|
+
parkComposer(): void;
|
|
220
|
+
/** Give a parked draft back once no dialog needs the keyboard. */
|
|
221
|
+
restoreComposer(): void;
|
|
222
|
+
/** How the selected session's record is being read right now. */
|
|
223
|
+
get view(): ViewState;
|
|
224
|
+
/** Show a detached history window, releasing the one it replaces.
|
|
225
|
+
* @param window - Record to display, or undefined to return to the live transcript.
|
|
226
|
+
*/
|
|
227
|
+
setViewWindow(window?: Transcript): void;
|
|
228
|
+
/** Move the reader's position inside the displayed record.
|
|
229
|
+
* @param scroll - Rows scrolled back from the live end.
|
|
230
|
+
*/
|
|
231
|
+
setScroll(scroll: number): void;
|
|
232
|
+
/** Replace the set of expanded reasoning blocks, keyed by message sequence.
|
|
233
|
+
* @param folds - Sequences to expand beyond the default fold.
|
|
234
|
+
*/
|
|
235
|
+
setFolds(folds: ReadonlySet<number>): void;
|
|
236
|
+
/** Set the fold mode of the live attempt's completed reasoning.
|
|
237
|
+
* @param reasoning - `row` to fold, `full` to keep the streamed text.
|
|
238
|
+
*/
|
|
239
|
+
setLiveReasoning(reasoning: Reasoning): void;
|
|
240
|
+
/** Local answer state for the selected session's pending waterfalls. */
|
|
241
|
+
get interaction(): InteractionState;
|
|
242
|
+
/** Replace the partly collected answers, keyed by the waterfall event id.
|
|
243
|
+
* @param answers - Answers collected so far, by event id.
|
|
244
|
+
*/
|
|
245
|
+
setAnswers(answers: Record<string, ObjectValue[]>): void;
|
|
246
|
+
/** Replace the pending question's option keyboard state.
|
|
247
|
+
* @param option - Highlighted option, toggled labels and free-text mode; undefined clears it.
|
|
248
|
+
*/
|
|
249
|
+
setOption(option?: OptionState): void;
|
|
250
|
+
/** Replace the pending approval's selected row.
|
|
251
|
+
* @param approval - Selected approval row; undefined clears the highlight.
|
|
252
|
+
*/
|
|
253
|
+
setApproval(approval?: InteractionState['approval']): void;
|
|
254
|
+
/** Composer-adjacent `@` reference menu state. */
|
|
255
|
+
get reference(): ReferenceState;
|
|
256
|
+
/** Highlight one row of the open reference menu.
|
|
257
|
+
* @param index - Row index into the current matches.
|
|
258
|
+
*/
|
|
259
|
+
setReferenceIndex(index: number): void;
|
|
260
|
+
/** Remember the draft that dismissed the reference menu, so it does not reopen while it stands.
|
|
261
|
+
* @param draft - Composer text at dismissal, or undefined to allow the menu again.
|
|
262
|
+
*/
|
|
263
|
+
setReferenceDismissed(draft?: string): void;
|
|
264
|
+
/** Panels the selected session has open. */
|
|
265
|
+
get panels(): PanelState;
|
|
266
|
+
/** Show or hide the reasoning panel.
|
|
267
|
+
* @param open - Whether `/think` is open.
|
|
268
|
+
*/
|
|
269
|
+
openThoughts(open: boolean): void;
|
|
270
|
+
/** Show or hide the pending-input panel.
|
|
271
|
+
* @param open - Whether `/queue` is open.
|
|
272
|
+
*/
|
|
273
|
+
openQueue(open: boolean): void;
|
|
274
|
+
/** Show the model dialog at one step, or close it.
|
|
275
|
+
* @param model - Catalog plus the provider or model being inspected; undefined closes the dialog.
|
|
276
|
+
*/
|
|
277
|
+
setModelPanel(model?: ModelState): void;
|
|
278
|
+
/** Show the history or content-search dialog, or close it.
|
|
279
|
+
* @param history - Query, content-search mode and matches; undefined closes the dialog.
|
|
280
|
+
*/
|
|
281
|
+
setHistoryPanel(history?: PanelState['history']): void;
|
|
282
|
+
/** Show the host session-search results, or close them.
|
|
283
|
+
* @param search - Query, results and truncation flag; undefined closes the dialog.
|
|
284
|
+
*/
|
|
285
|
+
setSearchPanel(search?: PanelState['search']): void;
|
|
286
|
+
/** Remember a locally submitted command, which never becomes a durable session record. */
|
|
287
|
+
recordRecall(value: string): void;
|
|
288
|
+
/** Leave recall navigation because the composer was edited or replaced. */
|
|
289
|
+
resetRecall(): void;
|
|
290
|
+
/** Whether recall is parked on the oldest prompt it retains in memory. */
|
|
291
|
+
get recallAtOldest(): boolean;
|
|
292
|
+
/** How many prompts the session retains for recall. */
|
|
293
|
+
get recallLength(): number;
|
|
294
|
+
/** Whether an older prompt is reachable at all: shed from the window, or still on the host. */
|
|
295
|
+
get recallHasOlder(): boolean;
|
|
296
|
+
/** Recover older prompts from the loaded window before spending a page request.
|
|
297
|
+
*
|
|
298
|
+
* The transcript window can still hold prompts the index's budgets evicted, so a backward step
|
|
299
|
+
* refills from memory first and only then asks the caller to page. That is what keeps recall
|
|
300
|
+
* complete across submissions, where the previous buffer lost them at the eviction boundary.
|
|
301
|
+
* @returns Whether any older prompt was recovered.
|
|
302
|
+
*/
|
|
303
|
+
refillRecall(): boolean;
|
|
304
|
+
/** Seed recall from a complete cached entry, so an open that follows a scan costs no request.
|
|
305
|
+
* @param sessionId - Session being opened.
|
|
306
|
+
* @returns Whether the cache covered this session.
|
|
307
|
+
*/
|
|
308
|
+
private adoptCachedPrompts;
|
|
309
|
+
/** Fold one history page the cost scan already read into the prompt cache.
|
|
310
|
+
*
|
|
311
|
+
* The scan reads every session's whole history on connect, so this is where two readers stop
|
|
312
|
+
* paying twice: it hands over pages it already has, and an open that follows reads the cache.
|
|
313
|
+
* @param sessionId - Session the page belongs to.
|
|
314
|
+
* @param records - Raw records of one scanned page.
|
|
315
|
+
*/
|
|
316
|
+
rememberScanPage(sessionId: string, records: readonly Json[]): void;
|
|
317
|
+
/** Report that the scanned session's history was read to its beginning.
|
|
318
|
+
* @param sessionId - Session the scan finished.
|
|
319
|
+
*/
|
|
320
|
+
rememberScanDone(sessionId: string): void;
|
|
321
|
+
/** Fold every prompt the host still holds into the recall index, in the background.
|
|
322
|
+
*
|
|
323
|
+
* Session start delivers only the newest window, so without this the arrows could reach older
|
|
324
|
+
* prompts but not show them without paging first. Each page is parsed into a temporary transcript
|
|
325
|
+
* and only its prompts are kept, so the live record, its memory window and the row cache never
|
|
326
|
+
* grow. The walk is bounded and the next selection cancels it; anything past the bound is still
|
|
327
|
+
* reachable through the lazy backward step.
|
|
328
|
+
* @param sessionId - Session being opened.
|
|
329
|
+
* @param selection - Selector generation that must still be current.
|
|
330
|
+
*/
|
|
331
|
+
private backfillPrompts;
|
|
192
332
|
/** Search one page at a time, preserving only the first 200 matches and releasing temporary content.
|
|
193
333
|
* @param query - Literal, case-insensitive text including folded reasoning.
|
|
194
334
|
* @param signal - Cancels HTTP and processing without cancelling the agent.
|