@itookit/dsht 0.3.3 → 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 +10 -10
- package/README.zh.md +7 -7
- package/dist/cli/dsht.d.ts +2 -0
- package/dist/cli/dsht.js +125 -0
- package/dist/cli/index.js +16 -123
- package/dist/controller/controller.d.ts +115 -0
- package/dist/controller/controller.js +126 -1
- package/dist/controller/perf-measures.d.ts +34 -0
- package/dist/controller/perf-measures.js +78 -0
- 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 +157 -1
- package/dist/session/controller.js +395 -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/navigation.d.ts +62 -8
- package/dist/session/navigation.js +72 -13
- 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 +230 -178
- package/dist/ui/chat/status.js +15 -10
- package/dist/ui/dialogs/index.d.ts +9 -8
- package/dist/ui/dialogs/index.js +3 -3
- package/dist/ui/dialogs/picker.d.ts +21 -3
- package/dist/ui/dialogs/picker.js +37 -5
- package/dist/ui/input/input.d.ts +18 -3
- package/dist/ui/input/input.js +61 -22
- package/dist/ui/input/viewport.d.ts +96 -0
- package/dist/ui/input/viewport.js +173 -0
- package/dsht-m.png +0 -0
- package/package.json +3 -3
- package/dist/ui/input/history.d.ts +0 -19
- package/dist/ui/input/history.js +0 -43
|
@@ -36,15 +36,20 @@ export function resolveTarget(items, query, id, names) {
|
|
|
36
36
|
}
|
|
37
37
|
/** Classify one session from the host's list summary; nothing is inferred from silence.
|
|
38
38
|
* @param session - Session summary from `session/list`.
|
|
39
|
-
* @
|
|
39
|
+
* @param pending - Whether this client holds an unanswered interaction for that session.
|
|
40
|
+
* @returns Needs-you while an answer is owed, running while its agent works, otherwise idle or blank.
|
|
40
41
|
*/
|
|
41
|
-
export function sessionState(session) {
|
|
42
|
+
export function sessionState(session, pending = false) {
|
|
43
|
+
if (pending)
|
|
44
|
+
return 'needs';
|
|
42
45
|
if (session.running === true)
|
|
43
46
|
return 'running';
|
|
44
47
|
return session.blank === true ? 'blank' : 'idle';
|
|
45
48
|
}
|
|
46
|
-
/** Leading marker per state: a working clock, a filled
|
|
47
|
-
export const SESSION_MARKERS = { running: '◐', idle: '●', blank: '○' };
|
|
49
|
+
/** Leading marker per state: a question mark, a working clock, a filled dot, and an unused circle. */
|
|
50
|
+
export const SESSION_MARKERS = { needs: '?', running: '◐', idle: '●', blank: '○' };
|
|
51
|
+
/** One word per state, so every screen names the same state the same way. */
|
|
52
|
+
export const STATE_LABELS = { needs: 'needs you', running: 'working', idle: 'ready', blank: 'empty' };
|
|
48
53
|
/** Coarse age of a session's last activity, so the column stays steady between list refreshes.
|
|
49
54
|
* @param time - Epoch milliseconds of the last activity, when the summary reported one.
|
|
50
55
|
* @param now - Current epoch milliseconds.
|
|
@@ -65,20 +70,74 @@ export function activityAge(time, now) {
|
|
|
65
70
|
/** Status cell for one session row: its state marker and the age of its last activity.
|
|
66
71
|
* @param session - Session summary from `session/list`.
|
|
67
72
|
* @param now - Current epoch milliseconds.
|
|
73
|
+
* @param pending - Whether this client holds an unanswered interaction for that session.
|
|
68
74
|
* @returns Marker with an optional age, without a trailing space when unknown.
|
|
69
75
|
*/
|
|
70
|
-
export function sessionStatus(session, now) {
|
|
76
|
+
export function sessionStatus(session, now, pending = false) {
|
|
71
77
|
const age = activityAge(typeof session.updatedAt === 'number' ? session.updatedAt : undefined, now);
|
|
72
|
-
return `${SESSION_MARKERS[sessionState(session)]}${age === '' ? '' : ` ${age}`}`;
|
|
78
|
+
return `${SESSION_MARKERS[sessionState(session, pending)]}${age === '' ? '' : ` ${age}`}`;
|
|
73
79
|
}
|
|
80
|
+
/** States a workspace rollup reports, most actionable first. */
|
|
81
|
+
export const ROLLUP_STATES = ['needs', 'running', 'idle'];
|
|
74
82
|
/** Count the sessions of one workspace by the state each reports.
|
|
83
|
+
*
|
|
84
|
+
* Blank sessions are counted by neither a badge nor a word: a session that never sent a turn is the
|
|
85
|
+
* absence of activity, and listing it beside real work only makes the rollup harder to read.
|
|
75
86
|
* @param sessions - Sessions whose `sessionIds` belong to the workspace.
|
|
76
|
-
* @
|
|
87
|
+
* @param pending - Session IDs this client holds an unanswered interaction for.
|
|
88
|
+
* @returns One count per state that occurs, most actionable first, or an empty list.
|
|
77
89
|
*/
|
|
78
|
-
export function
|
|
79
|
-
const counts = {
|
|
80
|
-
for (const session of sessions)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
export function workspaceCounts(sessions, pending = new Set()) {
|
|
91
|
+
const counts = { needs: 0, running: 0, idle: 0 };
|
|
92
|
+
for (const session of sessions) {
|
|
93
|
+
const id = session.sessionId;
|
|
94
|
+
const state = sessionState(session, typeof id === 'string' && pending.has(id));
|
|
95
|
+
if (state !== 'blank')
|
|
96
|
+
counts[state] += 1;
|
|
97
|
+
}
|
|
98
|
+
return ROLLUP_STATES.filter(state => counts[state] > 0).map(state => ({ state, count: counts[state] }));
|
|
99
|
+
}
|
|
100
|
+
/** Render one rollup as separately coloured cells.
|
|
101
|
+
*
|
|
102
|
+
* Each cell after the first carries the separator that joins it to the previous one, so a caller can
|
|
103
|
+
* colour the cells independently without losing the text {@link workspaceStatus} would produce.
|
|
104
|
+
* @param counts - Counts from {@link workspaceCounts}.
|
|
105
|
+
* @param style - `words` spells each state out; `badges` keeps only the marker and the count.
|
|
106
|
+
* @returns The cells in the order given, with their separators.
|
|
107
|
+
*/
|
|
108
|
+
export function workspaceSegments(counts, style = 'words') {
|
|
109
|
+
const separator = style === 'words' ? ' · ' : ' ';
|
|
110
|
+
return counts.map(({ state, count }, index) => ({ state,
|
|
111
|
+
text: `${index === 0 ? '' : separator}${style === 'words' ? `${SESSION_MARKERS[state]} ${count} ${STATE_LABELS[state]}` : `${SESSION_MARKERS[state]}${count}`}` }));
|
|
112
|
+
}
|
|
113
|
+
/** Render one rollup as plain text, the same way every screen and test reads it.
|
|
114
|
+
* @param counts - Counts from {@link workspaceCounts}.
|
|
115
|
+
* @param style - `words` spells each state out; `badges` keeps only the marker and the count.
|
|
116
|
+
* @returns The joined cell text, empty when nothing was counted.
|
|
117
|
+
*/
|
|
118
|
+
export function workspaceStatus(counts, style = 'words') {
|
|
119
|
+
return workspaceSegments(counts, style).map(segment => segment.text).join('');
|
|
120
|
+
}
|
|
121
|
+
/** Marker key for the compact rollup, which has no room for the words. */
|
|
122
|
+
export const ROLLUP_LEGEND = `${SESSION_MARKERS.idle} ${STATE_LABELS.idle} · ${SESSION_MARKERS.running} ${STATE_LABELS.running} · ${SESSION_MARKERS.needs} ${STATE_LABELS.needs}`;
|
|
123
|
+
/** Secondary path text for one workspace row.
|
|
124
|
+
*
|
|
125
|
+
* The title is usually the last path segment, so repeating it wastes the row; the parent directory
|
|
126
|
+
* is what distinguishes two checkouts. A title that does not name the last segment keeps the full
|
|
127
|
+
* path, because dropping it would hide where the workspace actually lives.
|
|
128
|
+
* @param path - Registered host directory.
|
|
129
|
+
* @param title - Workspace title as the row already shows it.
|
|
130
|
+
* @returns The path to show beside the row, or an empty string when nothing is left.
|
|
131
|
+
*/
|
|
132
|
+
export function workspaceDetail(path, title) {
|
|
133
|
+
if (!path)
|
|
134
|
+
return '';
|
|
135
|
+
const trimmed = path.replace(/\/+$/u, '');
|
|
136
|
+
const cut = trimmed.lastIndexOf('/');
|
|
137
|
+
const base = cut < 0 ? trimmed : trimmed.slice(cut + 1);
|
|
138
|
+
if (base !== title)
|
|
139
|
+
return path;
|
|
140
|
+
if (cut > 0)
|
|
141
|
+
return trimmed.slice(0, cut);
|
|
142
|
+
return cut === 0 ? '/' : '';
|
|
84
143
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HistoryLimits } from './memory.ts';
|
|
2
|
-
import {
|
|
2
|
+
import type { PromptRecord } from './info.ts';
|
|
3
|
+
import { type Json, type ObjectValue } from '../transport/wire.ts';
|
|
3
4
|
interface ToolSummary {
|
|
4
5
|
name: string;
|
|
5
6
|
operation?: string;
|
|
@@ -19,6 +20,21 @@ export declare function toolLine(text: string, width: number): string;
|
|
|
19
20
|
* @returns Blocks joined by newlines.
|
|
20
21
|
*/
|
|
21
22
|
export declare function contentText(content: Json | undefined, tools?: ReadonlyMap<string, ToolSummary>, width?: number, reasoning?: 'row' | 'full'): string;
|
|
23
|
+
/** One durable `user/message` event as a recallable prompt.
|
|
24
|
+
*
|
|
25
|
+
* Injected context and every other record type return undefined. This is the single place the rule
|
|
26
|
+
* lives, so a prompt parsed straight from a wire record (the cost scan's pages) is identical to one
|
|
27
|
+
* the transcript folded into its own window.
|
|
28
|
+
* @param seq - Durable sequence of the record.
|
|
29
|
+
* @param event - Decoded wire event, when the record carried one.
|
|
30
|
+
* @returns The prompt text, or undefined when the record is not a user prompt.
|
|
31
|
+
*/
|
|
32
|
+
export declare function eventPrompt(seq: number, event: ObjectValue | undefined): PromptRecord | undefined;
|
|
33
|
+
/** User prompts in one raw history page, oldest first.
|
|
34
|
+
* @param records - One HTTP history page's records.
|
|
35
|
+
* @returns Prompts with their durable sequences, in page order.
|
|
36
|
+
*/
|
|
37
|
+
export declare function recordPrompts(records: unknown): PromptRecord[];
|
|
22
38
|
/** Semantic content stays separate from terminal rows, styles, and fold state. */
|
|
23
39
|
export interface MessagePart {
|
|
24
40
|
kind: 'text' | 'reasoning' | 'tool' | 'success' | 'error';
|
|
@@ -144,6 +160,26 @@ export declare class Transcript {
|
|
|
144
160
|
get thoughts(): ThoughtEntry[];
|
|
145
161
|
/** Latest loaded user prompt summary, sharing the durable thought index. */
|
|
146
162
|
get latestPrompt(): string;
|
|
163
|
+
/** User prompts newer than `afterSeq`, oldest first, with the newest sequence scanned.
|
|
164
|
+
*
|
|
165
|
+
* Recall folds its tail this way instead of projecting messages, so a stream frame never rebuilds
|
|
166
|
+
* the row layout at a second width just to notice a new prompt. The watermark is returned even when
|
|
167
|
+
* the scanned records contributed no prompt, because an assistant-only turn must not make the next
|
|
168
|
+
* frame rescan it.
|
|
169
|
+
* @param afterSeq - Newest sequence already folded into the caller's index.
|
|
170
|
+
* @returns Prompts with an increasing sequence, and the watermark the caller should adopt.
|
|
171
|
+
*/
|
|
172
|
+
promptsSince(afterSeq: number): {
|
|
173
|
+
prompts: PromptRecord[];
|
|
174
|
+
through: number;
|
|
175
|
+
};
|
|
176
|
+
/** User prompts strictly older than `beforeSeq`, oldest first, from the retained window.
|
|
177
|
+
* @param beforeSeq - Sequence the caller's index has already reached.
|
|
178
|
+
* @returns Loaded prompts that can refill an evicted prefix without a page request.
|
|
179
|
+
*/
|
|
180
|
+
promptsBefore(beforeSeq: number): PromptRecord[];
|
|
181
|
+
/** One `user/message` record as a prompt; injected context and other records return undefined. */
|
|
182
|
+
private userPrompt;
|
|
147
183
|
/** Number of semantic records and unfinished legacy chunks held by the client. */
|
|
148
184
|
get retainedRecordCount(): number;
|
|
149
185
|
/** Earliest loaded record, used with the opening cursor for backward paging. */
|
|
@@ -59,6 +59,40 @@ export function contentText(content, tools, width = 100, reasoning = 'full') {
|
|
|
59
59
|
}
|
|
60
60
|
/** Event types that contribute a displayed message; other retained events only affect live state. */
|
|
61
61
|
const DISPLAY_EVENTS = new Set(['user/message', 'assistant/message', 'tool/result']);
|
|
62
|
+
/** One durable `user/message` event as a recallable prompt.
|
|
63
|
+
*
|
|
64
|
+
* Injected context and every other record type return undefined. This is the single place the rule
|
|
65
|
+
* lives, so a prompt parsed straight from a wire record (the cost scan's pages) is identical to one
|
|
66
|
+
* the transcript folded into its own window.
|
|
67
|
+
* @param seq - Durable sequence of the record.
|
|
68
|
+
* @param event - Decoded wire event, when the record carried one.
|
|
69
|
+
* @returns The prompt text, or undefined when the record is not a user prompt.
|
|
70
|
+
*/
|
|
71
|
+
export function eventPrompt(seq, event) {
|
|
72
|
+
if (!event || event.type !== 'user/message' || event.surfaceOp !== 'append')
|
|
73
|
+
return undefined;
|
|
74
|
+
const data = object(event.data);
|
|
75
|
+
if (data.source && object(data.source).kind !== 'user')
|
|
76
|
+
return undefined;
|
|
77
|
+
return { seq, text: contentText(data.content) };
|
|
78
|
+
}
|
|
79
|
+
/** User prompts in one raw history page, oldest first.
|
|
80
|
+
* @param records - One HTTP history page's records.
|
|
81
|
+
* @returns Prompts with their durable sequences, in page order.
|
|
82
|
+
*/
|
|
83
|
+
export function recordPrompts(records) {
|
|
84
|
+
const prompts = [];
|
|
85
|
+
for (const raw of array(records)) {
|
|
86
|
+
const event = object(object(raw).event);
|
|
87
|
+
const seq = event.seq;
|
|
88
|
+
if (typeof seq !== 'number' || !Number.isSafeInteger(seq))
|
|
89
|
+
continue;
|
|
90
|
+
const prompt = eventPrompt(seq, event);
|
|
91
|
+
if (prompt)
|
|
92
|
+
prompts.push(prompt);
|
|
93
|
+
}
|
|
94
|
+
return prompts;
|
|
95
|
+
}
|
|
62
96
|
/** Opening snapshots replace all state; durable events are deduplicated by sequence. */
|
|
63
97
|
export class Transcript {
|
|
64
98
|
sizes = new Map();
|
|
@@ -397,6 +431,45 @@ export class Transcript {
|
|
|
397
431
|
}
|
|
398
432
|
/** Latest loaded user prompt summary, sharing the durable thought index. */
|
|
399
433
|
get latestPrompt() { void this.thoughts; return this.thoughtIndex.prompt; }
|
|
434
|
+
/** User prompts newer than `afterSeq`, oldest first, with the newest sequence scanned.
|
|
435
|
+
*
|
|
436
|
+
* Recall folds its tail this way instead of projecting messages, so a stream frame never rebuilds
|
|
437
|
+
* the row layout at a second width just to notice a new prompt. The watermark is returned even when
|
|
438
|
+
* the scanned records contributed no prompt, because an assistant-only turn must not make the next
|
|
439
|
+
* frame rescan it.
|
|
440
|
+
* @param afterSeq - Newest sequence already folded into the caller's index.
|
|
441
|
+
* @returns Prompts with an increasing sequence, and the watermark the caller should adopt.
|
|
442
|
+
*/
|
|
443
|
+
promptsSince(afterSeq) {
|
|
444
|
+
const keys = this.sortedKeys();
|
|
445
|
+
let start = keys.length;
|
|
446
|
+
while (start > 0 && keys[start - 1] > afterSeq)
|
|
447
|
+
start--;
|
|
448
|
+
const prompts = [];
|
|
449
|
+
for (let index = start; index < keys.length; index++) {
|
|
450
|
+
const prompt = this.userPrompt(keys[index]);
|
|
451
|
+
if (prompt)
|
|
452
|
+
prompts.push(prompt);
|
|
453
|
+
}
|
|
454
|
+
return { prompts, through: Math.max(afterSeq, keys.at(-1) ?? afterSeq) };
|
|
455
|
+
}
|
|
456
|
+
/** User prompts strictly older than `beforeSeq`, oldest first, from the retained window.
|
|
457
|
+
* @param beforeSeq - Sequence the caller's index has already reached.
|
|
458
|
+
* @returns Loaded prompts that can refill an evicted prefix without a page request.
|
|
459
|
+
*/
|
|
460
|
+
promptsBefore(beforeSeq) {
|
|
461
|
+
const prompts = [];
|
|
462
|
+
for (const seq of this.sortedKeys()) {
|
|
463
|
+
if (seq >= beforeSeq)
|
|
464
|
+
break;
|
|
465
|
+
const prompt = this.userPrompt(seq);
|
|
466
|
+
if (prompt)
|
|
467
|
+
prompts.push(prompt);
|
|
468
|
+
}
|
|
469
|
+
return prompts;
|
|
470
|
+
}
|
|
471
|
+
/** One `user/message` record as a prompt; injected context and other records return undefined. */
|
|
472
|
+
userPrompt(seq) { return eventPrompt(seq, this.events.get(seq)); }
|
|
400
473
|
/** Number of semantic records and unfinished legacy chunks held by the client. */
|
|
401
474
|
get retainedRecordCount() { return this.events.size; }
|
|
402
475
|
/** Earliest loaded record, used with the opening cursor for backward paging. */
|
package/dist/state.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Application state shared by the controller facade and every domain controller. */
|
|
2
|
-
import {
|
|
2
|
+
import { SessionInfo } from './session/info.ts';
|
|
3
3
|
import type { HistorySearch, RemovalTarget } from './session/types.ts';
|
|
4
4
|
import type { ObjectValue } from './transport/wire.ts';
|
|
5
5
|
/** State shared by the picker and conversation view. */
|
|
@@ -21,7 +21,8 @@ export interface State {
|
|
|
21
21
|
presetError?: string;
|
|
22
22
|
presets?: ObjectValue[];
|
|
23
23
|
defaultModel?: ObjectValue;
|
|
24
|
-
|
|
24
|
+
/** Record, prompt index, composer, view and interaction state of the selected session. */
|
|
25
|
+
session: SessionInfo;
|
|
25
26
|
}
|
|
26
27
|
/** The state contract every domain controller writes through. */
|
|
27
28
|
export interface ControllerStore {
|
package/dist/state.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Application state shared by the controller facade and every domain controller. */
|
|
2
|
-
import {
|
|
2
|
+
import { SessionInfo } from "./session/info.js";
|
|
3
3
|
/** Build the initial state before any connection exists.
|
|
4
4
|
* @returns A fresh state whose transcript is empty and disconnected.
|
|
5
5
|
*/
|
|
6
6
|
export function initialState() {
|
|
7
7
|
return { version: 0, online: false, busy: false, screen: 'workspaces', status: 'Connecting…',
|
|
8
|
-
error: '', workspaces: [], sessions: [], showAllSessions: false, pending: [],
|
|
8
|
+
error: '', workspaces: [], sessions: [], showAllSessions: false, pending: [], session: new SessionInfo() };
|
|
9
9
|
}
|