@itookit/dsht 0.5.1 → 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.
- package/README.i18n.yaml +2 -2
- package/README.md +4 -2
- package/README.zh.md +6 -4
- package/dist/catalog/controller.d.ts +26 -6
- package/dist/catalog/controller.js +73 -45
- package/dist/catalog/index.d.ts +1 -0
- package/dist/cli/dsht.js +4 -1
- package/dist/cli/startup.js +30 -11
- package/dist/cli/verifier.d.ts +4 -0
- package/dist/cli/verifier.js +28 -5
- package/dist/contracts.d.ts +20 -5
- package/dist/controller/connection-streams.d.ts +22 -0
- package/dist/controller/connection-streams.js +105 -0
- package/dist/controller/connection.d.ts +14 -3
- package/dist/controller/connection.js +40 -69
- package/dist/controller/controller.d.ts +17 -233
- package/dist/controller/controller.js +108 -811
- package/dist/controller/foreground.d.ts +44 -0
- package/dist/controller/foreground.js +79 -0
- package/dist/controller/loop-coordinator.d.ts +48 -0
- package/dist/controller/loop-coordinator.js +647 -0
- package/dist/controller/verifier.d.ts +4 -0
- package/dist/cost/controller.d.ts +1 -1
- package/dist/cost/controller.js +12 -5
- package/dist/cost/scanner.js +1 -0
- package/dist/session/controller.d.ts +23 -35
- package/dist/session/controller.js +113 -363
- package/dist/session/history-reader.d.ts +32 -0
- package/dist/session/history-reader.js +170 -0
- package/dist/session/index.d.ts +1 -1
- package/dist/session/info.d.ts +3 -38
- package/dist/session/info.js +14 -1
- package/dist/session/interactions.d.ts +26 -0
- package/dist/session/interactions.js +75 -0
- package/dist/session/navigator.d.ts +47 -0
- package/dist/session/navigator.js +158 -0
- package/dist/session/prompt-backfill.d.ts +23 -0
- package/dist/session/prompt-backfill.js +88 -0
- package/dist/session/state.d.ts +20 -0
- package/dist/session/state.js +1 -0
- package/dist/session/telemetry.d.ts +15 -6
- package/dist/session/telemetry.js +44 -7
- package/dist/session/transcript.d.ts +5 -1
- package/dist/slash/index.d.ts +1 -1
- package/dist/slash/parse.d.ts +2 -126
- package/dist/slash/registry.d.ts +1 -1
- package/dist/slash/types.d.ts +126 -0
- package/dist/slash/types.js +1 -0
- package/dist/state.d.ts +5 -17
- package/dist/state.js +1 -1
- package/dist/transport/client.d.ts +4 -3
- package/dist/transport/client.js +71 -25
- package/dist/ui/app.js +86 -301
- package/dist/ui/chat/shell-view.d.ts +2 -0
- package/dist/ui/chat/shell-view.js +8 -0
- package/dist/ui/chat/use-history-view.d.ts +69 -0
- package/dist/ui/chat/use-history-view.js +123 -0
- package/dist/ui/dialogs/use-panels.d.ts +53 -0
- package/dist/ui/dialogs/use-panels.js +51 -0
- package/dist/ui/input/use-composer.d.ts +35 -0
- package/dist/ui/input/use-composer.js +109 -0
- package/dist/ui/input/use-deferred-lines.d.ts +16 -0
- package/dist/ui/input/use-deferred-lines.js +54 -0
- package/dist/ui/input/use-history-recall.d.ts +20 -0
- package/dist/ui/input/use-history-recall.js +47 -0
- package/package.json +1 -1
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: 02d559e19c88336d6eb1f1e1ff80999bbfaa7274
|
|
3
|
+
README.zh.md: b962a44f8a85716f4bd35f8afd0af7d140ffe0ca
|
package/README.md
CHANGED
|
@@ -398,6 +398,8 @@ Closed `mermaid` fences render as Unicode diagrams for supported flowcharts, sta
|
|
|
398
398
|
|
|
399
399
|
`/export-html [local.html]` saves the currently loaded conversation and live tail with Mermaid SVG images and MathJax-generated MathML. Open the file in a browser for full mathematical layout; no network or scripts are required. Older or evicted messages are excluded, and tool rows remain summaries. The default filename is timestamped in the working directory; quoted paths are accepted, existing files are never overwritten, and cancellation removes incomplete output. `/export` remains the complete host-log ZIP download.
|
|
400
400
|
|
|
401
|
+
While disconnected, you can keep editing your draft and save, edit, or delete local shortcut prompts. Host operations report that the connection is unavailable and retain the draft. Reconnecting refreshes data and restores subscriptions while preserving the selected conversation, picker filter, or host-path screen; it never automatically submits an unsent draft.
|
|
402
|
+
|
|
401
403
|
## Live status
|
|
402
404
|
|
|
403
405
|
The footer groups `◐ Working · 8s · Ctrl+C Stop`, `● Ready`, or `? Needs you` while this client still owes an approval or an answer, model and reasoning effort, the session cost beside today's spend with the all-time total, a ten-cell context bar and percentage, and session turns / total tokens / cache-hit share. Wide terminals reserve the activity column so model and metrics stay aligned when a run completes. Narrow terminals reclaim padding, remove the bar, shorten the model, and then omit lower-priority metrics while retaining the stop hint. `/status` shows the host URL, operation status, workspace path, full provider/model, pending model, usage buckets, turns, queues, jobs, and four-decimal costs. `!` flags a metrics or model catalog error, or incomplete cost coverage; details explain the cause. Running sessions use the last-used model; ready sessions use the next selection, with the host catalog default for new sessions. Model catalog changes refresh on host settings, credential, and adapter notifications.
|
|
@@ -436,7 +438,7 @@ Usage files live under `~/.local/state/dsht/cost/<origin-hash>/` (respecting `XD
|
|
|
436
438
|
|
|
437
439
|
## Client API
|
|
438
440
|
|
|
439
|
-
Installed packages export `Client` from `@itookit/dsht` and `login`/`CookieStore` from `@itookit/dsht/auth`, with TypeScript declarations. Source consumers can import from `src/transport/client.ts` with a TypeScript loader, or from `dist/index.js` after building. `authenticate(token)` exchanges credentials; `connect()` opens one multiplexed socket; `listWorkspaces()` and `listSessions(workspaceId?)` return promises of server rows. `call(endpoint, args, signal?)` preserves host errors as `RemoteError` with `code` and `details`. Always await `close()` in `finally
|
|
441
|
+
Installed packages export `Client` from `@itookit/dsht` and `login`/`CookieStore` from `@itookit/dsht/auth`, with TypeScript declarations. Source consumers can import from `src/transport/client.ts` with a TypeScript loader, or from `dist/index.js` after building. `authenticate(token)` exchanges credentials; `connect()` opens one multiplexed socket; `listWorkspaces(signal?)` and `listSessions(workspaceId?, signal?)` return promises of server rows. `call(endpoint, args, signal?)` preserves host errors as `RemoteError` with `code` and `details`. Always await `close()` in `finally`; concurrent calls share completion. Closing permanently ends that client, so create a new `Client` for a new lifetime. A peer disconnect without `close()` allows `connect()` again with the retained cookie. Library consumers opt into persistence with `login(client, token, new CookieStore())` from `src/transport/auth.ts`; `Client.authenticate()` itself only retains credentials in memory.
|
|
440
442
|
|
|
441
443
|
Session and workspace command methods use `{ request: { ... } }` inside `args`; session listing uses `{ _request: {} }`. `$events/result` uses its named arguments directly. Follow snapshots replace retained state after reconnect; durable messages and transient assistant text remain separate. The reader accepts both `event` records and older `chunks` wrappers containing `chunkrow/text-chunks`, `chunkrow/reasoning-chunks`, or `chunkrow/tool-call-chunks`. Hosts without `assistantStream` expose live text through logged chunks; the TUI reconstructs only the unfinished attempt and preserves each packed record's starting sequence for pagination.
|
|
442
444
|
|
|
@@ -484,7 +486,7 @@ node dist/cli/index.js --help
|
|
|
484
486
|
|
|
485
487
|
Tests use isolated HTTP/WebSocket hosts, drive the real Ink picker and composer, run the CLI in subprocesses, and project copied Harness v2 workspace-edit and v0 packed-chunk recordings. The repository needs no model credentials for these checks. The recording and expected transcript live under `tests/`; they do not depend on a parent checkout. Live model-provider behavior is not covered by these tests.
|
|
486
488
|
|
|
487
|
-
Source is organised by business domain under `src/`: `transport/` owns the host wire protocol and normalizes host frames into semantic events, `session/` the transcript, history, interactions and the session runtime, `cost/` the billing ledger, `catalog/` models and presets, `shell/` local `!` commands, `controller/` the application facade and event routing, `slash/` the command syntax, `ui/` everything React and Ink, `storage/` every filesystem operation, `cli/` the composition root, and the root files (`state.ts`, `json.ts`, `text.ts`, `contracts.ts`, `session-title.ts`, `references.ts`) the shared contracts.
|
|
489
|
+
Source is organised by business domain under `src/`: `transport/` owns the host wire protocol and normalizes host frames into semantic events, `session/` the transcript, history, interactions and the session runtime, `cost/` the billing ledger, `catalog/` models and presets, `shell/` local `!` commands, `controller/` the application facade and event routing, `slash/` the command syntax, `ui/` everything React and Ink, `storage/` every filesystem operation, `cli/` the composition root, and the root files (`state.ts`, `json.ts`, `text.ts`, `contracts.ts`, `session-title.ts`, `references.ts`) the shared contracts. Internal cross-domain imports follow the allowed dependency directions, using a domain entry point or a specific module. UI leaves read domain types through `contracts.ts`. The TypeScript syntax check in `tests/architecture/dependencies.test.ts` enforces nine forbidden directions, type-only contracts, resolvable literal module targets, and the absence of dependency cycles, including type edges.
|
|
488
490
|
|
|
489
491
|
`npm test` renders frames without styling, because the assertions and the recorded expectations in `tests/expected/` describe text. A test runner started from a terminal exports `FORCE_COLOR=1` to each test file, which makes Ink interleave SGR escapes between a prompt and its text; `npm run test:terminal` reproduces that environment on any host, and `prepublishOnly` runs it so a publish from a terminal validates what a terminal actually renders. Theme tests render separate truecolor and plain subprocesses with terminal and CI color detection isolated from the parent environment.
|
|
490
492
|
|
package/README.zh.md
CHANGED
|
@@ -279,8 +279,8 @@ npx @itookit/dsht list sessions --workspace WORKSPACE_ID --json
|
|
|
279
279
|
```sh
|
|
280
280
|
npm start -- list workspaces --json
|
|
281
281
|
npm start -- list sessions --json
|
|
282
|
-
node --import tsx src/cli/index.
|
|
283
|
-
node --import tsx src/cli/index.
|
|
282
|
+
node --import tsx src/cli/index.ts list workspaces --json
|
|
283
|
+
node --import tsx src/cli/index.ts list sessions --json
|
|
284
284
|
```
|
|
285
285
|
|
|
286
286
|
JSON 输出格式为 `{ "items": [...] }`;省略 `--json` 则输出制表符分隔的列表。工作区筛选使用服务端 `sessionIds` 成员关系。工作区列表读取 `workspace/follow` 的首个 baseline 后取消订阅,不会调用不存在的 `workspace/list` 端点。
|
|
@@ -398,6 +398,8 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
398
398
|
|
|
399
399
|
`/export-html [local.html]` 将当前已加载会话和实时尾部保存为包含 Mermaid SVG 图片及 MathJax 生成的 MathML 的页面。用浏览器打开文件可查看完整数学排版,不需要网络或脚本。尚未加载或已回收的消息不包含在内,工具行仍为摘要。默认文件名带时间戳并保存在当前工作目录,支持带引号的路径,不覆盖已有文件,取消时删除未完成输出。`/export` 仍下载服务端完整日志 ZIP。
|
|
400
400
|
|
|
401
|
+
断线期间仍可编辑草稿,保存、编辑或删除本地快捷提示词。需要服务端的操作会报告未连接并保留草稿。重连只刷新数据和恢复订阅,保留当前会话、选择器筛选或路径输入页面,不会自动发送未提交的草稿。
|
|
402
|
+
|
|
401
403
|
## 实时状态
|
|
402
404
|
|
|
403
405
|
底栏分组显示 `◐ Working · 8s · Ctrl+C Stop`、`● Ready`,或本客户端还欠一个审批/回答时的 `? Needs you`、模型与思考强度、本会话费用与「今天花费(历史总计)」、十格上下文进度条及百分比、会话轮次与累计 token 及缓存命中率。宽终端为运行状态预留固定宽度,完成后模型和指标保持对齐。窄终端依次回收留白、隐藏进度条、缩短模型名、省略次要指标,优先保留停止提示。`/status` 显示主机 URL、操作状态、工作区完整路径、完整供应商/模型、下次模型、各项用量、轮次、队列、后台任务和四位小数费用。`!` 表示有指标或模型目录错误,或计费覆盖不完整;详情中显示原因。运行中使用最近实际使用的模型,空闲时使用下次模型,新会话使用服务端模型目录默认值。服务端设置、凭据和适配器变更通知会刷新模型目录。
|
|
@@ -436,7 +438,7 @@ Slash 命令在选择器和对话输入框中均可使用。输入 `/` 会显示
|
|
|
436
438
|
|
|
437
439
|
## 客户端接口
|
|
438
440
|
|
|
439
|
-
安装后的包通过 `@itookit/dsht` 导出 `Client`,通过 `@itookit/dsht/auth` 导出 `login`/`CookieStore`,并提供 TypeScript 声明。源码调用方可通过 TypeScript loader 从 `src/transport/client.ts` 导入,或构建后从 `dist/index.js` 导入。`authenticate(token)` 兑换凭据;`connect()` 打开一条多路复用连接;`listWorkspaces()` 和 `listSessions(workspaceId?)` 返回服务端列表的 Promise。`call(endpoint, args, signal?)` 将服务端错误保留为带有 `code` 和 `details` 的 `RemoteError`。务必在 `finally` 中等待 `close()`。库调用方可使用 `src/transport/auth.ts` 的 `login(client, token, new CookieStore())` 启用持久化;`Client.authenticate()` 本身仅在内存中保留凭据。
|
|
441
|
+
安装后的包通过 `@itookit/dsht` 导出 `Client`,通过 `@itookit/dsht/auth` 导出 `login`/`CookieStore`,并提供 TypeScript 声明。源码调用方可通过 TypeScript loader 从 `src/transport/client.ts` 导入,或构建后从 `dist/index.js` 导入。`authenticate(token)` 兑换凭据;`connect()` 打开一条多路复用连接;`listWorkspaces(signal?)` 和 `listSessions(workspaceId?, signal?)` 返回服务端列表的 Promise。`call(endpoint, args, signal?)` 将服务端错误保留为带有 `code` 和 `details` 的 `RemoteError`。务必在 `finally` 中等待 `close()`;并发调用等待同一次关闭完成。关闭会永久结束该客户端,下一次使用需新建 `Client`。若只是对端断线而未调用 `close()`,可保留 Cookie 再次 `connect()`。库调用方可使用 `src/transport/auth.ts` 的 `login(client, token, new CookieStore())` 启用持久化;`Client.authenticate()` 本身仅在内存中保留凭据。
|
|
440
442
|
|
|
441
443
|
会话和工作区命令在 `args` 内使用 `{ request: { ... } }`;会话列表使用 `{ _request: {} }`。`$events/result` 直接使用具名参数。重连后的 follow 快照整体替换保留状态;持久消息与临时助手文本分别保存。读取器同时支持 `event` 记录和旧版 `chunks` 包装;后者包含 `chunkrow/text-chunks`、`chunkrow/reasoning-chunks` 或 `chunkrow/tool-call-chunks`。不提供 `assistantStream` 的服务端通过日志 chunk 传递实时文本;TUI 只重建尚未完成的尝试,并保留每条压缩记录的起始序号用于翻页。
|
|
442
444
|
|
|
@@ -484,7 +486,7 @@ node dist/cli/index.js --help
|
|
|
484
486
|
|
|
485
487
|
测试使用隔离的 HTTP/WebSocket 服务,驱动实际 Ink 选择器和输入框,在子进程中运行 CLI,并投影复制的 Harness v2 工作区编辑记录和 v0 压缩 chunk 记录。这些检查不需要模型凭据。记录和预期对话输出位于 `tests/`,不依赖父仓库。测试不覆盖真实模型供应商行为。
|
|
486
488
|
|
|
487
|
-
源码在 `src/` 下按业务域组织:`transport/` 负责服务端 wire 协议并把宿主帧归一化为语义事件,`session/` 负责对话、历史、交互与会话运行态,`cost/` 负责计费账本,`catalog/` 负责模型与 preset,`shell/` 负责本地 `!` 命令,`controller/` 是应用门面与事件路由,`slash/` 负责命令语法,`ui/` 承载全部 React 与 Ink,`storage/` 负责全部文件系统操作,`cli/` 是组装入口,根文件(`state.ts`、`json.ts`、`text.ts`、`contracts.ts`、`session-title.ts`、`references.ts
|
|
489
|
+
源码在 `src/` 下按业务域组织:`transport/` 负责服务端 wire 协议并把宿主帧归一化为语义事件,`session/` 负责对话、历史、交互与会话运行态,`cost/` 负责计费账本,`catalog/` 负责模型与 preset,`shell/` 负责本地 `!` 命令,`controller/` 是应用门面与事件路由,`slash/` 负责命令语法,`ui/` 承载全部 React 与 Ink,`storage/` 负责全部文件系统操作,`cli/` 是组装入口,根文件(`state.ts`、`json.ts`、`text.ts`、`contracts.ts`、`session-title.ts`、`references.ts`)是共享契约。内部跨域导入沿允许的依赖方向,可使用域入口或具体模块;UI 叶子组件通过 `contracts.ts` 读取领域类型。`tests/architecture/dependencies.test.ts` 使用 TypeScript 语法解析,检查九条禁止方向、纯类型契约、可解析的字面量模块路径,以及包含类型边在内的无环依赖。
|
|
488
490
|
|
|
489
491
|
`npm test` 渲染不带样式的帧,因为断言和 `tests/expected/` 中的预期输出描述的是文本。从终端启动的测试运行器会向每个测试文件导出 `FORCE_COLOR=1`,使 Ink 在提示符与文本之间插入 SGR 转义序列;`npm run test:terminal` 在任何主机上复现该环境,`prepublishOnly` 也会运行它,因此从终端发布时验证的就是终端实际渲染的结果。 主题测试在独立子进程中分别渲染真彩色和纯文本,并隔离父进程中影响终端和 CI 颜色检测的环境设置。
|
|
490
492
|
|
|
@@ -1,32 +1,52 @@
|
|
|
1
1
|
import type { HostAccess } from '../transport/host.ts';
|
|
2
2
|
import { type ObjectValue } from '../transport/wire.ts';
|
|
3
|
-
|
|
3
|
+
/** Catalog can publish its metadata and selection result, but cannot navigate or mutate sessions. */
|
|
4
|
+
export interface CatalogUpdate {
|
|
5
|
+
defaultModel?: ObjectValue;
|
|
6
|
+
modelError?: string;
|
|
7
|
+
presets?: ObjectValue[];
|
|
8
|
+
presetError?: string;
|
|
9
|
+
status?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CatalogHost extends HostAccess {
|
|
12
|
+
selection(): {
|
|
13
|
+
revision: number;
|
|
14
|
+
sessionId: string | undefined;
|
|
15
|
+
};
|
|
16
|
+
publish(patch: CatalogUpdate): void;
|
|
17
|
+
}
|
|
4
18
|
/** Owns model-catalog and preset loads for the selected session. */
|
|
5
19
|
export declare class CatalogController {
|
|
6
|
-
private readonly store;
|
|
7
20
|
private readonly host;
|
|
8
21
|
private presetClient?;
|
|
9
22
|
private revision;
|
|
23
|
+
private generation;
|
|
24
|
+
private refreshAbort?;
|
|
10
25
|
private tasks;
|
|
11
|
-
constructor(
|
|
26
|
+
constructor(host: CatalogHost);
|
|
12
27
|
/** Drop generation-scoped catalog state at the start of a connection generation. */
|
|
13
28
|
reset(): void;
|
|
29
|
+
/** Stop catalog requests before waiting for transport shutdown. Reset opens the next generation. */
|
|
30
|
+
close(): void;
|
|
14
31
|
/** Wait for every in-flight catalog task, so shutdown leaves no pending request. */
|
|
15
32
|
settle(): Promise<void>;
|
|
33
|
+
private track;
|
|
34
|
+
private signal;
|
|
16
35
|
/** Load the optional preset roster once per connection, only when a session names a preset. */
|
|
17
36
|
loadPresetNames(): void;
|
|
18
37
|
/** Fetch current model routes and adapter-owned reasoning choices for the selected session.
|
|
19
38
|
* @returns Host catalog; provider failures remain available to the selector.
|
|
20
39
|
*/
|
|
21
|
-
modelCatalog(): Promise<ObjectValue>;
|
|
40
|
+
modelCatalog(caller?: AbortSignal): Promise<ObjectValue>;
|
|
22
41
|
/** Select the next request's model; the host also attempts to save its deployment default.
|
|
23
42
|
* @param provider - Host provider route ID.
|
|
24
43
|
* @param model - Exact model ID.
|
|
25
44
|
* @param reasoningEffort - Optional adapter-owned effort ID; omission uses its default.
|
|
26
45
|
*/
|
|
27
|
-
selectModel(provider: string, model: string, reasoningEffort?: string): Promise<void>;
|
|
46
|
+
selectModel(provider: string, model: string, reasoningEffort?: string, caller?: AbortSignal): Promise<void>;
|
|
28
47
|
/** Reload the default route and provider failures without touching session state. */
|
|
29
48
|
refresh(): void;
|
|
30
49
|
/** @returns The selected session identity, or a `Select a session first` failure. */
|
|
31
|
-
private
|
|
50
|
+
private selected;
|
|
51
|
+
private isSelected;
|
|
32
52
|
}
|
|
@@ -1,88 +1,116 @@
|
|
|
1
1
|
import { array, errorText, object, string } from "../transport/wire.js";
|
|
2
2
|
/** Owns model-catalog and preset loads for the selected session. */
|
|
3
3
|
export class CatalogController {
|
|
4
|
-
store;
|
|
5
4
|
host;
|
|
6
5
|
presetClient;
|
|
7
6
|
revision = 0;
|
|
7
|
+
generation = new AbortController();
|
|
8
|
+
refreshAbort;
|
|
8
9
|
tasks = new Set();
|
|
9
|
-
constructor(
|
|
10
|
-
this.store = store;
|
|
10
|
+
constructor(host) {
|
|
11
11
|
this.host = host;
|
|
12
12
|
}
|
|
13
13
|
/** Drop generation-scoped catalog state at the start of a connection generation. */
|
|
14
14
|
reset() {
|
|
15
|
-
this.
|
|
15
|
+
this.close();
|
|
16
|
+
this.generation = new AbortController();
|
|
17
|
+
this.presetClient = undefined;
|
|
18
|
+
this.host.publish({ modelError: undefined, defaultModel: undefined, presets: undefined, presetError: undefined });
|
|
16
19
|
}
|
|
20
|
+
/** Stop catalog requests before waiting for transport shutdown. Reset opens the next generation. */
|
|
21
|
+
close() { this.revision++; this.generation.abort(); }
|
|
17
22
|
/** Wait for every in-flight catalog task, so shutdown leaves no pending request. */
|
|
18
|
-
async settle() { await Promise.
|
|
23
|
+
async settle() { await Promise.allSettled(this.tasks); }
|
|
24
|
+
track(task) {
|
|
25
|
+
this.tasks.add(task);
|
|
26
|
+
void task.then(() => this.tasks.delete(task), () => this.tasks.delete(task));
|
|
27
|
+
return task;
|
|
28
|
+
}
|
|
29
|
+
signal(caller = this.host.signal()) {
|
|
30
|
+
return AbortSignal.any([caller, this.host.signal(), this.generation.signal]);
|
|
31
|
+
}
|
|
19
32
|
/** Load the optional preset roster once per connection, only when a session names a preset. */
|
|
20
33
|
loadPresetNames() {
|
|
21
34
|
const client = this.host.client();
|
|
22
|
-
|
|
35
|
+
const signal = this.signal();
|
|
36
|
+
if (!client || !this.host.online() || this.presetClient === client || signal.aborted)
|
|
23
37
|
return;
|
|
24
38
|
this.presetClient = client;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
const current = () => !signal.aborted && client === this.host.client();
|
|
40
|
+
const task = client.call('agentPresets/list', {}, signal).then(value => {
|
|
41
|
+
if (current())
|
|
42
|
+
this.host.publish({ presets: array(object(value).presets).map(object), presetError: undefined });
|
|
28
43
|
}).catch(error => {
|
|
29
|
-
if (
|
|
30
|
-
this.
|
|
44
|
+
if (current())
|
|
45
|
+
this.host.publish({ presets: [], presetError: errorText(error) });
|
|
31
46
|
});
|
|
32
|
-
this.
|
|
33
|
-
void task.finally(() => this.tasks.delete(task));
|
|
47
|
+
this.track(task);
|
|
34
48
|
}
|
|
35
49
|
/** Fetch current model routes and adapter-owned reasoning choices for the selected session.
|
|
36
50
|
* @returns Host catalog; provider failures remain available to the selector.
|
|
37
51
|
*/
|
|
38
|
-
async modelCatalog() {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
async modelCatalog(caller) {
|
|
53
|
+
const selection = this.selected();
|
|
54
|
+
const signal = this.signal(caller);
|
|
55
|
+
signal.throwIfAborted();
|
|
56
|
+
return this.track((async () => {
|
|
57
|
+
const value = object(await this.host.require().call('session/modelCatalog', {}, signal));
|
|
58
|
+
signal.throwIfAborted();
|
|
59
|
+
if (!this.isSelected(selection))
|
|
60
|
+
throw new Error('Session changed while loading models');
|
|
61
|
+
return value;
|
|
62
|
+
})());
|
|
45
63
|
}
|
|
46
64
|
/** Select the next request's model; the host also attempts to save its deployment default.
|
|
47
65
|
* @param provider - Host provider route ID.
|
|
48
66
|
* @param model - Exact model ID.
|
|
49
67
|
* @param reasoningEffort - Optional adapter-owned effort ID; omission uses its default.
|
|
50
68
|
*/
|
|
51
|
-
async selectModel(provider, model, reasoningEffort) {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
async selectModel(provider, model, reasoningEffort, caller) {
|
|
70
|
+
const selection = this.selected();
|
|
71
|
+
const signal = this.signal(caller);
|
|
72
|
+
signal.throwIfAborted();
|
|
73
|
+
await this.track((async () => {
|
|
74
|
+
const selected = object(object(await this.host.require().call('session/selectModel', { request: {
|
|
75
|
+
sessionId: selection.sessionId, provider, model, ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
|
|
76
|
+
} }, signal)).selected);
|
|
77
|
+
signal.throwIfAborted();
|
|
78
|
+
if (!this.isSelected(selection))
|
|
79
|
+
return;
|
|
80
|
+
this.host.publish({ status: `Next request: ${string(selected.provider)} / ${string(selected.model)}${selected.reasoningEffort ? ` · ${string(selected.reasoningEffort)}` : ''}` });
|
|
81
|
+
this.refresh();
|
|
82
|
+
})());
|
|
61
83
|
}
|
|
62
84
|
/** Reload the default route and provider failures without touching session state. */
|
|
63
85
|
refresh() {
|
|
64
86
|
const client = this.host.client();
|
|
65
|
-
if (!client)
|
|
87
|
+
if (!client || !this.host.online())
|
|
88
|
+
return;
|
|
89
|
+
this.refreshAbort?.abort();
|
|
90
|
+
this.refreshAbort = new AbortController();
|
|
91
|
+
const signal = this.signal(this.refreshAbort.signal);
|
|
92
|
+
if (signal.aborted)
|
|
66
93
|
return;
|
|
67
94
|
const revision = ++this.revision;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (client === this.host.client() && revision === this.revision)
|
|
73
|
-
this.store.update({ defaultModel: undefined, modelError: errorText(error) });
|
|
95
|
+
const current = () => !signal.aborted && client === this.host.client() && revision === this.revision;
|
|
96
|
+
const task = client.call('session/modelCatalog', {}, signal).then(value => {
|
|
97
|
+
if (current())
|
|
98
|
+
this.host.publish({ defaultModel: object(object(value).default), modelError: undefined });
|
|
74
99
|
}).catch(error => {
|
|
75
|
-
if (
|
|
76
|
-
this.
|
|
100
|
+
if (current())
|
|
101
|
+
this.host.publish({ defaultModel: undefined, modelError: errorText(error) });
|
|
77
102
|
});
|
|
78
|
-
this.
|
|
79
|
-
void task.finally(() => this.tasks.delete(task));
|
|
103
|
+
this.track(task);
|
|
80
104
|
}
|
|
81
105
|
/** @returns The selected session identity, or a `Select a session first` failure. */
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
106
|
+
selected() {
|
|
107
|
+
const selected = this.host.selection();
|
|
108
|
+
if (!selected.sessionId)
|
|
85
109
|
throw new Error('Select a session first');
|
|
86
|
-
return
|
|
110
|
+
return { ...selected, sessionId: selected.sessionId };
|
|
111
|
+
}
|
|
112
|
+
isSelected(selection) {
|
|
113
|
+
const current = this.host.selection();
|
|
114
|
+
return current.revision === selection.revision && current.sessionId === selection.sessionId;
|
|
87
115
|
}
|
|
88
116
|
}
|
package/dist/catalog/index.d.ts
CHANGED
package/dist/cli/dsht.js
CHANGED
|
@@ -168,7 +168,10 @@ async function main() {
|
|
|
168
168
|
cwd: localDirectory, env: process.env,
|
|
169
169
|
timeoutMs: verifyTimeoutMs(process.env.DSHT_VERIFY_TIMEOUT_MS),
|
|
170
170
|
createSession: (title) => controller.actions.createVerifierSession(title),
|
|
171
|
-
cancelSession: async (sessionId) => {
|
|
171
|
+
cancelSession: async (sessionId) => {
|
|
172
|
+
if (!await controller.actions.cancelVerifierSession(sessionId))
|
|
173
|
+
throw new Error('Verifier cancellation was not accepted');
|
|
174
|
+
},
|
|
172
175
|
onLine: line => { if (values.headless)
|
|
173
176
|
log(line); },
|
|
174
177
|
// Off by default: a verifier reason reaches the progress line and the trace, and that log may be
|
package/dist/cli/startup.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { runCommand } from "../controller/index.js";
|
|
8
8
|
import { errorText } from "../transport/wire.js";
|
|
9
9
|
import { dirname } from 'node:path';
|
|
10
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
10
11
|
import { ensureDirectory, renameFile, writePrivateFile } from "../storage/index.js";
|
|
11
12
|
import { latestAssistantText } from "../controller/loop.js";
|
|
12
13
|
import { parseVerdict } from "../controller/loop-contract.js";
|
|
@@ -26,12 +27,15 @@ const VERDICT_POLL_MS = 200;
|
|
|
26
27
|
* @param what - Human-readable subject for the timeout message.
|
|
27
28
|
* @param timeoutMs - Longest wait.
|
|
28
29
|
*/
|
|
29
|
-
async function until(condition, what, timeoutMs = STEP_TIMEOUT_MS) {
|
|
30
|
+
async function until(signal, condition, what, timeoutMs = STEP_TIMEOUT_MS) {
|
|
30
31
|
const deadline = Date.now() + timeoutMs;
|
|
31
|
-
|
|
32
|
+
for (;;) {
|
|
33
|
+
signal.throwIfAborted();
|
|
34
|
+
if (condition())
|
|
35
|
+
return;
|
|
32
36
|
if (Date.now() >= deadline)
|
|
33
37
|
throw new Error(`Timed out waiting for ${what}`);
|
|
34
|
-
await
|
|
38
|
+
await delay(POLL_MS, undefined, { signal });
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
/** Run the plan against a started controller.
|
|
@@ -41,9 +45,10 @@ async function until(condition, what, timeoutMs = STEP_TIMEOUT_MS) {
|
|
|
41
45
|
* @returns Whether a started loop passed, nothing ran, or the run failed.
|
|
42
46
|
*/
|
|
43
47
|
export async function runStartup(controller, plan, log) {
|
|
48
|
+
const signal = controller.connection.signal();
|
|
44
49
|
// `online` alone is too early: the startup picker runs after it and would overwrite a selection
|
|
45
50
|
// made here, which is exactly how a forked verifier used to lose its session.
|
|
46
|
-
await until(() => controller.state.online && controller.queries.connectionSettled, 'the host connection');
|
|
51
|
+
await until(signal, () => controller.state.online && controller.queries.connectionSettled, 'the host connection');
|
|
47
52
|
if (plan.workspace !== undefined && !await controller.actions.switchWorkspace(plan.workspace)) {
|
|
48
53
|
throw new Error(`Workspace not found: ${plan.workspace}`);
|
|
49
54
|
}
|
|
@@ -60,12 +65,13 @@ export async function runStartup(controller, plan, log) {
|
|
|
60
65
|
}
|
|
61
66
|
// A selected session is not sendable until its follow snapshot lands.
|
|
62
67
|
if (plan.commands.length) {
|
|
63
|
-
await until(() => controller.state.screen === 'chat' && controller.state.sessionId !== undefined && controller.queries.record.ready, 'the session snapshot');
|
|
68
|
+
await until(signal, () => controller.state.screen === 'chat' && controller.state.sessionId !== undefined && controller.queries.record.ready, 'the session snapshot');
|
|
64
69
|
}
|
|
65
70
|
// Start-up lines are one-shot action commands; the cancellable port is only used by searches
|
|
66
71
|
// and exports, which have no meaning before the operator is present.
|
|
67
72
|
const port = { run: (_label, operation) => operation(controller.connection.signal()) };
|
|
68
73
|
for (const line of plan.commands) {
|
|
74
|
+
signal.throwIfAborted();
|
|
69
75
|
// The scripted half runs the same two stages the composer does after `interpret`: a headless
|
|
70
76
|
// caller has no menus or screens, but it has every application fact `normalize`/`authorize` read.
|
|
71
77
|
const command = normalize({ kind: 'line', line }, {
|
|
@@ -100,14 +106,19 @@ export async function runStartup(controller, plan, log) {
|
|
|
100
106
|
// Sending a prompt needs the follow snapshot, and a verifier starts the instant a busy turn ends,
|
|
101
107
|
// so this wait is longer than a normal startup step: the host can be slow to open the new stream.
|
|
102
108
|
try {
|
|
103
|
-
await until(() => controller.state.sessionId !== undefined && controller.queries.record.ready, 'the verifier session snapshot', PROMPT_SNAPSHOT_TIMEOUT_MS);
|
|
109
|
+
await until(signal, () => controller.state.sessionId !== undefined && controller.queries.record.ready, 'the verifier session snapshot', PROMPT_SNAPSHOT_TIMEOUT_MS);
|
|
104
110
|
}
|
|
105
111
|
catch (error) {
|
|
112
|
+
signal.throwIfAborted();
|
|
106
113
|
// Which of the three conditions failed is the whole diagnosis, so it travels with the error.
|
|
107
114
|
throw new Error(`${errorText(error)} (screen=${controller.state.screen}, session=${controller.state.sessionId ?? 'none'},`
|
|
108
115
|
+ ` snapshot=${String(controller.queries.record.ready)}, online=${String(controller.state.online)})`);
|
|
109
116
|
}
|
|
110
|
-
await controller.actions.prompt(plan.prompt)
|
|
117
|
+
if (!await controller.actions.prompt(plan.prompt)) {
|
|
118
|
+
signal.throwIfAborted();
|
|
119
|
+
const reason = controller.state.lastFailure;
|
|
120
|
+
throw new Error(`Prompt was not accepted${reason ? ` (${reason})` : ''}`);
|
|
121
|
+
}
|
|
111
122
|
}
|
|
112
123
|
if (controller.queries.loop !== undefined)
|
|
113
124
|
return await waitForLoop(controller, plan.timeoutSeconds * 1000, log);
|
|
@@ -129,8 +140,10 @@ export async function runStartup(controller, plan, log) {
|
|
|
129
140
|
* @returns The verdict once the line may run, or the refusal it will never outlive.
|
|
130
141
|
*/
|
|
131
142
|
async function authorizeWhenReady(controller, command) {
|
|
143
|
+
const signal = controller.connection.signal();
|
|
132
144
|
const deadline = Date.now() + STEP_TIMEOUT_MS;
|
|
133
145
|
for (;;) {
|
|
146
|
+
signal.throwIfAborted();
|
|
134
147
|
const verdict = authorize(command, {
|
|
135
148
|
sessionSelected: controller.state.sessionId !== undefined,
|
|
136
149
|
pending: controller.state.pending.length > 0,
|
|
@@ -141,7 +154,7 @@ async function authorizeWhenReady(controller, command) {
|
|
|
141
154
|
return verdict;
|
|
142
155
|
if (Date.now() >= deadline)
|
|
143
156
|
throw new Error(`Timed out waiting for the client to be free: ${command.kind}`);
|
|
144
|
-
await
|
|
157
|
+
await delay(POLL_MS, undefined, { signal });
|
|
145
158
|
}
|
|
146
159
|
}
|
|
147
160
|
/** Persist the verdict this session's reply just produced.
|
|
@@ -157,6 +170,8 @@ async function authorizeWhenReady(controller, command) {
|
|
|
157
170
|
* @param log - Progress sink.
|
|
158
171
|
*/
|
|
159
172
|
async function writeVerdict(controller, target, repliesBefore, log) {
|
|
173
|
+
const signal = controller.connection.signal();
|
|
174
|
+
signal.throwIfAborted();
|
|
160
175
|
const [, kind = '', step = '', attempt = ''] = target.identity.split('/');
|
|
161
176
|
const expect = { verificationId: target.identity, kind, step: Number(step), attempt: Number(attempt) };
|
|
162
177
|
// The host reports the turn idle just before that reply is committed, so an immediate parse can
|
|
@@ -169,7 +184,7 @@ async function writeVerdict(controller, target, repliesBefore, log) {
|
|
|
169
184
|
let parsed = replyArrived() ? parseVerdict(latestAssistantText(controller.queries.record.messages), expect) : undefined;
|
|
170
185
|
const deadline = Date.now() + VERDICT_GRACE_MS;
|
|
171
186
|
while (parsed === undefined && Date.now() < deadline) {
|
|
172
|
-
await
|
|
187
|
+
await delay(VERDICT_POLL_MS, undefined, { signal });
|
|
173
188
|
if (replyArrived())
|
|
174
189
|
parsed = parseVerdict(latestAssistantText(controller.queries.record.messages), expect);
|
|
175
190
|
}
|
|
@@ -209,8 +224,10 @@ async function writeVerdict(controller, target, repliesBefore, log) {
|
|
|
209
224
|
* @returns `idle` when the turn ended, `failed` on timeout.
|
|
210
225
|
*/
|
|
211
226
|
async function waitForTurn(controller, finishedBefore, timeoutMs, log) {
|
|
227
|
+
const signal = controller.connection.signal();
|
|
212
228
|
const deadline = Date.now() + timeoutMs;
|
|
213
229
|
for (;;) {
|
|
230
|
+
signal.throwIfAborted();
|
|
214
231
|
if (controller.queries.turnsCompleted > finishedBefore) {
|
|
215
232
|
log('Turn finished');
|
|
216
233
|
return 'idle';
|
|
@@ -226,7 +243,7 @@ async function waitForTurn(controller, finishedBefore, timeoutMs, log) {
|
|
|
226
243
|
log('Turn timed out');
|
|
227
244
|
return 'failed';
|
|
228
245
|
}
|
|
229
|
-
await
|
|
246
|
+
await delay(POLL_MS, undefined, { signal });
|
|
230
247
|
}
|
|
231
248
|
}
|
|
232
249
|
/** The host interaction this client cannot answer itself, when one is pending.
|
|
@@ -257,9 +274,11 @@ async function ensureWorkspace(controller, log) {
|
|
|
257
274
|
* @returns `passed` when the loop met its threshold, `failed` otherwise.
|
|
258
275
|
*/
|
|
259
276
|
async function waitForLoop(controller, timeoutMs, log) {
|
|
277
|
+
const signal = controller.connection.signal();
|
|
260
278
|
const deadline = Date.now() + timeoutMs;
|
|
261
279
|
let previous = '';
|
|
262
280
|
for (;;) {
|
|
281
|
+
signal.throwIfAborted();
|
|
263
282
|
const progress = controller.queries.loop;
|
|
264
283
|
if (progress === undefined || progress.phase !== 'running') {
|
|
265
284
|
// A cancelled loop usually means the connection ended; say so, or the exit code is a mystery.
|
|
@@ -290,6 +309,6 @@ async function waitForLoop(controller, timeoutMs, log) {
|
|
|
290
309
|
log('Loop timed out and was stopped');
|
|
291
310
|
return 'failed';
|
|
292
311
|
}
|
|
293
|
-
await
|
|
312
|
+
await delay(POLL_MS, undefined, { signal });
|
|
294
313
|
}
|
|
295
314
|
}
|
package/dist/cli/verifier.d.ts
CHANGED
|
@@ -51,10 +51,14 @@ export declare class ProcessVerifier implements VerifierPort {
|
|
|
51
51
|
private readonly options;
|
|
52
52
|
/** This client forks itself, so the harness that judges is this one. */
|
|
53
53
|
readonly name = "dsht";
|
|
54
|
+
private readonly tasks;
|
|
54
55
|
constructor(options: ProcessVerifierOptions);
|
|
55
56
|
/** @param request - Round identity, prompt and verdict path.
|
|
56
57
|
* @param signal - Cancels the child; the run also stops at its own deadline.
|
|
57
58
|
* @returns The verdict read back from the file, or a note explaining its absence.
|
|
58
59
|
*/
|
|
59
60
|
verify(request: VerifierRequest, signal: AbortSignal): Promise<VerifierOutcome>;
|
|
61
|
+
/** Keep host cancellation available until every child has completed its bounded cleanup. */
|
|
62
|
+
settle(): Promise<void>;
|
|
63
|
+
private runVerification;
|
|
60
64
|
}
|
package/dist/cli/verifier.js
CHANGED
|
@@ -66,6 +66,7 @@ export class ProcessVerifier {
|
|
|
66
66
|
options;
|
|
67
67
|
/** This client forks itself, so the harness that judges is this one. */
|
|
68
68
|
name = 'dsht';
|
|
69
|
+
tasks = new Set();
|
|
69
70
|
constructor(options) {
|
|
70
71
|
this.options = options;
|
|
71
72
|
}
|
|
@@ -74,6 +75,18 @@ export class ProcessVerifier {
|
|
|
74
75
|
* @returns The verdict read back from the file, or a note explaining its absence.
|
|
75
76
|
*/
|
|
76
77
|
async verify(request, signal) {
|
|
78
|
+
const task = this.runVerification(request, signal);
|
|
79
|
+
this.tasks.add(task);
|
|
80
|
+
try {
|
|
81
|
+
return await task;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.tasks.delete(task);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Keep host cancellation available until every child has completed its bounded cleanup. */
|
|
88
|
+
async settle() { await Promise.allSettled([...this.tasks]); }
|
|
89
|
+
async runVerification(request, signal) {
|
|
77
90
|
const run = this.options.run ?? runProcess;
|
|
78
91
|
const file = request.file;
|
|
79
92
|
// Cancelled before anything happened: no session, no child, no verdict file.
|
|
@@ -125,10 +138,18 @@ export class ProcessVerifier {
|
|
|
125
138
|
? { note: 'host cannot cancel', stopped: false }
|
|
126
139
|
: { note: 'no verifier session to cancel', stopped: true };
|
|
127
140
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
let timer;
|
|
142
|
+
let settled;
|
|
143
|
+
try {
|
|
144
|
+
settled = await Promise.race([
|
|
145
|
+
confirmation.then(() => true),
|
|
146
|
+
// This bounded wait is part of verify()/settle(), so it must keep the process alive.
|
|
147
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(false), CANCEL_CONFIRM_MS); }),
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
}
|
|
132
153
|
if (!settled)
|
|
133
154
|
return { note: `remote cancel unconfirmed after ${CANCEL_CONFIRM_MS} ms`, stopped: false };
|
|
134
155
|
return confirmed === true
|
|
@@ -197,8 +218,10 @@ export class ProcessVerifier {
|
|
|
197
218
|
cwd: this.options.cwd, env: this.options.env, signal: controller.signal, onLine,
|
|
198
219
|
});
|
|
199
220
|
// A cancelled review decides nothing, and is not a verifier outage either.
|
|
200
|
-
if (cancelled)
|
|
221
|
+
if (cancelled) {
|
|
222
|
+
await cancelNote();
|
|
201
223
|
return { type: 'cancelled' };
|
|
224
|
+
}
|
|
202
225
|
// The child stopped because the host wants a human: cancel the turn it left waiting (bounded,
|
|
203
226
|
// reported), and hand the request to the caller instead of retrying into the same block.
|
|
204
227
|
if (human !== undefined) {
|
package/dist/contracts.d.ts
CHANGED
|
@@ -12,11 +12,26 @@ export type { LivePhase, Message } from './session/transcript.ts';
|
|
|
12
12
|
export type { FileReference } from './references.ts';
|
|
13
13
|
export type { HistorySearch, RemovalTarget } from './session/types.ts';
|
|
14
14
|
export type { QueuedInput } from './session/telemetry.ts';
|
|
15
|
-
export type { ModelState, PanelState } from './session/info.ts';
|
|
16
15
|
export type { CostTotal, Coverage } from './cost/index.ts';
|
|
17
16
|
export type { ShellBlock } from './shell/index.ts';
|
|
18
|
-
import type {
|
|
19
|
-
import type {
|
|
17
|
+
import type { HistorySearch, RemovalTarget } from './session/types.ts';
|
|
18
|
+
import type { ObjectValue } from './json.ts';
|
|
19
|
+
/** Data delivered to dialogs; visibility and keyboard ownership belong to the UI. */
|
|
20
|
+
export interface ModelState {
|
|
21
|
+
catalog: ObjectValue;
|
|
22
|
+
provider?: string;
|
|
23
|
+
model?: ObjectValue;
|
|
24
|
+
}
|
|
25
|
+
export interface HistoryPanel {
|
|
26
|
+
query: string;
|
|
27
|
+
contentSearch: boolean;
|
|
28
|
+
matches?: HistorySearch;
|
|
29
|
+
}
|
|
30
|
+
export interface SearchPanel {
|
|
31
|
+
query: string;
|
|
32
|
+
items: ObjectValue[];
|
|
33
|
+
hasMore: boolean;
|
|
34
|
+
}
|
|
20
35
|
/** One user-saved shortcut prompt; the client owns the list, no session or host does. */
|
|
21
36
|
export interface SavedPrompt {
|
|
22
37
|
id: string;
|
|
@@ -263,10 +278,10 @@ export type ViewEffect =
|
|
|
263
278
|
/** Payload that opens its own panel. */
|
|
264
279
|
| {
|
|
265
280
|
kind: 'history';
|
|
266
|
-
history:
|
|
281
|
+
history: HistoryPanel | undefined;
|
|
267
282
|
} | {
|
|
268
283
|
kind: 'search';
|
|
269
|
-
search:
|
|
284
|
+
search: SearchPanel | undefined;
|
|
270
285
|
} | {
|
|
271
286
|
kind: 'model';
|
|
272
287
|
model: ModelState;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Event/control subscriptions and their startup deadlines belong to one connection generation. */
|
|
2
|
+
import { Client } from '../transport/client.ts';
|
|
3
|
+
import { type HostEvent } from '../transport/events.ts';
|
|
4
|
+
export interface StreamHost {
|
|
5
|
+
identified(clientId: string): void;
|
|
6
|
+
event(event: HostEvent): boolean;
|
|
7
|
+
changed(): void;
|
|
8
|
+
degraded(message: string): void;
|
|
9
|
+
fail(error: Error): void;
|
|
10
|
+
}
|
|
11
|
+
export declare class ConnectionStreams {
|
|
12
|
+
private readonly client;
|
|
13
|
+
private readonly host;
|
|
14
|
+
private readonly subscriptions;
|
|
15
|
+
private readonly abort;
|
|
16
|
+
private readonly signal;
|
|
17
|
+
constructor(client: Client, host: StreamHost, signal: AbortSignal);
|
|
18
|
+
start(): Promise<void>;
|
|
19
|
+
close(): void;
|
|
20
|
+
/** Resolve the first usable frame while retaining the stream until this generation ends. */
|
|
21
|
+
private follow;
|
|
22
|
+
}
|