agent-sh 0.14.1 → 0.14.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/dist/agent/agent-loop.d.ts +1 -1
- package/dist/agent/agent-loop.js +42 -31
- package/dist/agent/conversation-state.d.ts +3 -2
- package/dist/agent/conversation-state.js +20 -3
- package/dist/agent/events.d.ts +2 -0
- package/dist/agent/host-types.d.ts +3 -0
- package/dist/agent/index.js +2 -1
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/subagent.js +5 -1
- package/dist/agent/tool-protocol.d.ts +2 -2
- package/dist/agent/tool-protocol.js +5 -4
- package/dist/agent/tools/glob.d.ts +1 -1
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.d.ts +1 -1
- package/dist/agent/tools/grep.js +4 -2
- package/dist/agent/tools/ls.d.ts +1 -1
- package/dist/agent/tools/ls.js +4 -2
- package/dist/agent/tools/read-file.d.ts +1 -1
- package/dist/agent/tools/read-file.js +30 -2
- package/dist/agent/types.d.ts +11 -1
- package/dist/agent/types.js +6 -1
- package/dist/cli/index.js +0 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/settings.d.ts +3 -0
- package/dist/core/settings.js +2 -2
- package/dist/shell/index.d.ts +6 -0
- package/dist/shell/index.js +10 -10
- package/dist/shell/shell.d.ts +4 -0
- package/dist/shell/shell.js +15 -29
- package/dist/shell/terminal.d.ts +33 -0
- package/dist/shell/terminal.js +62 -0
- package/examples/extensions/ash-scheme/index.ts +2170 -0
- package/examples/extensions/ash-scheme/package.json +11 -0
- package/examples/extensions/ash-scheme-render.ts +58 -0
- package/examples/extensions/ashi/README.md +36 -26
- package/examples/extensions/ashi/package.json +9 -1
- package/examples/extensions/ashi/src/capture.ts +1 -0
- package/examples/extensions/ashi/src/cli.ts +21 -7
- package/examples/extensions/ashi/src/compaction.ts +25 -96
- package/examples/extensions/ashi/src/components.ts +64 -166
- package/examples/extensions/ashi/src/default-schema-renderers.ts +229 -0
- package/examples/extensions/ashi/src/display-config.ts +21 -22
- package/examples/extensions/ashi/src/frontend.ts +64 -65
- package/examples/extensions/ashi/src/hooks.ts +47 -63
- package/examples/extensions/ashi/src/multi-session-store.ts +44 -3
- package/examples/extensions/ashi/src/schema.ts +407 -0
- package/examples/extensions/ashi/src/session-store.ts +55 -4
- package/examples/extensions/ashi/src/status-footer.ts +27 -6
- package/examples/extensions/ashi-compact-llm.ts +93 -0
- package/examples/extensions/claude-code-bridge/index.ts +2 -0
- package/examples/extensions/opencode-bridge/index.ts +3 -0
- package/examples/extensions/opencode-provider.ts +252 -0
- package/examples/extensions/pi-bridge/index.ts +1 -0
- package/package.json +12 -1
- package/examples/extensions/ashi/src/default-renderers.ts +0 -171
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./components.js";
|
|
26
26
|
import type { ToolCallView, ToolResultView } from "./hooks.js";
|
|
27
27
|
import { createToolHookResolver } from "./hooks.js";
|
|
28
|
+
import { loadGroupMaxVisible } from "./display-config.js";
|
|
28
29
|
|
|
29
30
|
const GROUPABLE_KINDS = new Set(["read", "search"]);
|
|
30
31
|
const TOOL_KIND: Record<string, string> = {
|
|
@@ -120,6 +121,10 @@ function detailFromArgs(argsJson: string | undefined): string {
|
|
|
120
121
|
if (typeof args.path === "string") return relativize(args.path);
|
|
121
122
|
if (typeof args.file_path === "string") return relativize(args.file_path);
|
|
122
123
|
if (typeof args.query === "string") return `"${args.query}"`;
|
|
124
|
+
if (typeof args.source === "string") {
|
|
125
|
+
const compact = args.source.replace(/\s+/g, " ").trim();
|
|
126
|
+
return compact.length > 80 ? compact.slice(0, 77) + "…" : compact;
|
|
127
|
+
}
|
|
123
128
|
} catch { /* fall through */ }
|
|
124
129
|
return "";
|
|
125
130
|
}
|
|
@@ -229,10 +234,22 @@ export function mountAshi(
|
|
|
229
234
|
let activeAssistant: AssistantMessage | null = null;
|
|
230
235
|
let activeThinking: ThinkingBlock | null = null;
|
|
231
236
|
const activeTools = new Map<string, LiveToolEntry>();
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
237
|
+
const groupMaxVisible = loadGroupMaxVisible();
|
|
238
|
+
|
|
239
|
+
/** Find a same-kind ToolGroup at the chat tail. ThinkingBlocks are
|
|
240
|
+
* visually-neutral only when hidden — visible thinking is a hard separator,
|
|
241
|
+
* so toggling thinking on splits previously-merged groups at iteration
|
|
242
|
+
* boundaries. */
|
|
243
|
+
const findMergeableGroup = (kind: string): ToolGroup | null => {
|
|
244
|
+
for (let i = chat.children.length - 1; i >= 0; i--) {
|
|
245
|
+
const c = chat.children[i]!;
|
|
246
|
+
if (c instanceof ToolGroup) return c.kind === kind ? c : null;
|
|
247
|
+
if (c instanceof ThinkingBlock && hideThinking) continue;
|
|
248
|
+
if (c instanceof AssistantMessage && !c.hasContent()) continue;
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
};
|
|
236
253
|
let loader: Loader | null = null;
|
|
237
254
|
let processing = false;
|
|
238
255
|
const queuedQueries: string[] = [];
|
|
@@ -252,7 +269,7 @@ export function mountAshi(
|
|
|
252
269
|
invalidate: () => tui.requestRender(),
|
|
253
270
|
});
|
|
254
271
|
|
|
255
|
-
const tools = createToolHookResolver(ctx
|
|
272
|
+
const tools = createToolHookResolver(ctx);
|
|
256
273
|
|
|
257
274
|
const renderUserMessage = (text: string): Component =>
|
|
258
275
|
ctx.call("ashi:render-user-message", { text, ...renderState() }) as Component;
|
|
@@ -343,32 +360,18 @@ export function mountAshi(
|
|
|
343
360
|
chat.addChild(renderAssistantFinal(text));
|
|
344
361
|
}
|
|
345
362
|
if (m.tool_calls) {
|
|
346
|
-
const
|
|
347
|
-
let i = 0;
|
|
348
|
-
while (i < calls.length) {
|
|
349
|
-
const startName = calls[i]!.function?.name ?? "";
|
|
350
|
-
const startKind = TOOL_KIND[startName];
|
|
351
|
-
if (startKind && GROUPABLE_KINDS.has(startKind)) {
|
|
352
|
-
let j = i;
|
|
353
|
-
while (j < calls.length && TOOL_KIND[calls[j]!.function?.name ?? ""] === startKind) j++;
|
|
354
|
-
const runLen = j - i;
|
|
355
|
-
if (runLen > 1) {
|
|
356
|
-
const group = new ToolGroup(startKind, runLen);
|
|
357
|
-
chat.addChild(group);
|
|
358
|
-
for (let k = i; k < j; k++) {
|
|
359
|
-
const c = calls[k]!;
|
|
360
|
-
const cid = c.id ?? "";
|
|
361
|
-
const cname = c.function?.name ?? "tool";
|
|
362
|
-
group.addCall(cid, cname, detailFromArgs(c.function?.arguments));
|
|
363
|
-
if (cid) toolMap.set(cid, { kind: "group", group, name: cname });
|
|
364
|
-
}
|
|
365
|
-
i = j;
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
const tc = calls[i]!;
|
|
363
|
+
for (const tc of m.tool_calls) {
|
|
370
364
|
const id = tc.id ?? "";
|
|
371
365
|
const name = tc.function?.name ?? "tool";
|
|
366
|
+
const kind = TOOL_KIND[name];
|
|
367
|
+
if (kind && GROUPABLE_KINDS.has(kind)) {
|
|
368
|
+
const mergeable = findMergeableGroup(kind);
|
|
369
|
+
const group = mergeable
|
|
370
|
+
?? (() => { const g = new ToolGroup(kind, groupMaxVisible); chat.addChild(g); return g; })();
|
|
371
|
+
group.addCall(id, name, detailFromArgs(tc.function?.arguments));
|
|
372
|
+
if (id) toolMap.set(id, { kind: "group", group, name });
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
372
375
|
const pair = renderToolPair({
|
|
373
376
|
toolCallId: id, name, title: name, kind: undefined,
|
|
374
377
|
displayDetail: detailFromArgs(tc.function?.arguments),
|
|
@@ -377,8 +380,6 @@ export function mountAshi(
|
|
|
377
380
|
chat.addChild(pair.call);
|
|
378
381
|
chat.addChild(pair.result);
|
|
379
382
|
if (id) toolMap.set(id, { kind: "pair", pair, name });
|
|
380
|
-
lastToolResult = pair.result;
|
|
381
|
-
i++;
|
|
382
383
|
}
|
|
383
384
|
}
|
|
384
385
|
} else if (m.role === "tool") {
|
|
@@ -411,8 +412,6 @@ export function mountAshi(
|
|
|
411
412
|
activeAssistant = null;
|
|
412
413
|
activeThinking = null;
|
|
413
414
|
activeTools.clear();
|
|
414
|
-
batchGroups.clear();
|
|
415
|
-
lastToolResult = null;
|
|
416
415
|
chat.clear();
|
|
417
416
|
const branch = getStore().current().getBranch();
|
|
418
417
|
const toolMap = new Map<string, ReplayEntry>();
|
|
@@ -480,13 +479,6 @@ export function mountAshi(
|
|
|
480
479
|
tui.requestRender();
|
|
481
480
|
});
|
|
482
481
|
|
|
483
|
-
bus.on("agent:tool-batch", (e) => {
|
|
484
|
-
batchGroups.clear();
|
|
485
|
-
for (const g of e.groups) {
|
|
486
|
-
batchGroups.set(g.kind, { total: g.tools.length, group: null });
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
|
|
490
482
|
bus.on("agent:tool-started", (e) => {
|
|
491
483
|
finalizeThinking();
|
|
492
484
|
if (activeAssistant) {
|
|
@@ -495,34 +487,29 @@ export function mountAshi(
|
|
|
495
487
|
}
|
|
496
488
|
const id = e.toolCallId ?? `${e.title}-${Date.now()}`;
|
|
497
489
|
const title = e.title.split(":")[0]!.trim();
|
|
490
|
+
const lookupName = e.name ?? title;
|
|
498
491
|
const detail = e.displayDetail || detailFromArgs(
|
|
499
492
|
typeof e.rawInput === "string" ? e.rawInput : JSON.stringify(e.rawInput ?? {})
|
|
500
493
|
);
|
|
501
494
|
|
|
502
495
|
const kind = e.kind ?? "";
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
batchEntry!.group.addCall(id, title, detail);
|
|
511
|
-
activeTools.set(id, { kind: "group", group: batchEntry!.group });
|
|
512
|
-
// Grouped tools have no individual result body — Ctrl+O wouldn't have
|
|
513
|
-
// anything to expand, so leave lastToolResult pointing at the prior tool.
|
|
496
|
+
if (GROUPABLE_KINDS.has(kind)) {
|
|
497
|
+
const mergeable = findMergeableGroup(kind);
|
|
498
|
+
const group = mergeable
|
|
499
|
+
?? (() => { const g = new ToolGroup(kind, groupMaxVisible); chat.addChild(g); return g; })();
|
|
500
|
+
group.addCall(id, lookupName, detail);
|
|
501
|
+
activeTools.set(id, { kind: "group", group });
|
|
514
502
|
tui.requestRender();
|
|
515
503
|
return;
|
|
516
504
|
}
|
|
517
505
|
|
|
518
506
|
const pair = renderToolPair({
|
|
519
|
-
toolCallId: id, name:
|
|
507
|
+
toolCallId: id, name: lookupName, title, kind: e.kind,
|
|
520
508
|
displayDetail: detail, rawInput: e.rawInput,
|
|
521
509
|
});
|
|
522
510
|
activeTools.set(id, { kind: "pair", pair });
|
|
523
511
|
chat.addChild(pair.call);
|
|
524
512
|
chat.addChild(pair.result);
|
|
525
|
-
lastToolResult = pair.result;
|
|
526
513
|
tui.requestRender();
|
|
527
514
|
});
|
|
528
515
|
|
|
@@ -743,14 +730,21 @@ export function mountAshi(
|
|
|
743
730
|
// ── Keybindings ────────────────────────────────────────────────
|
|
744
731
|
const toggleThinking = (): void => {
|
|
745
732
|
hideThinking = !hideThinking;
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
733
|
+
if (processing) {
|
|
734
|
+
// Mid-turn: in-place show/hide only. Past groups stay as they merged
|
|
735
|
+
// under the old flag; future tool calls in this turn respect the new
|
|
736
|
+
// flag via findMergeableGroup. Next idle rebuild reflows everything.
|
|
737
|
+
const walk = (node: Container): void => {
|
|
738
|
+
for (const child of node.children) {
|
|
739
|
+
if (child instanceof ThinkingBlock) child.setHidden(hideThinking);
|
|
740
|
+
else if (child instanceof Container) walk(child);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
walk(chat);
|
|
744
|
+
tui.requestRender();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
void rebuildChat();
|
|
754
748
|
};
|
|
755
749
|
|
|
756
750
|
tui.addInputListener((data) => {
|
|
@@ -807,10 +801,15 @@ export function mountAshi(
|
|
|
807
801
|
return { consume: true };
|
|
808
802
|
}
|
|
809
803
|
if (matchesKey(data, "ctrl+o")) {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
804
|
+
const toggle = (node: Container): void => {
|
|
805
|
+
for (const child of node.children) {
|
|
806
|
+
const fn = (child as unknown as { toggleExpanded?: () => void }).toggleExpanded;
|
|
807
|
+
if (typeof fn === "function") fn.call(child);
|
|
808
|
+
if (child instanceof Container) toggle(child);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
toggle(chat);
|
|
812
|
+
tui.requestRender();
|
|
814
813
|
return { consume: true };
|
|
815
814
|
}
|
|
816
815
|
return undefined;
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import type { Component } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { ExtensionContext } from "agent-sh/types";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
ToolResultBody,
|
|
7
|
-
UserMessage,
|
|
8
|
-
} from "./components.js";
|
|
9
|
-
import { entryFor, loadToolDisplayConfig, type ToolResultMode } from "./display-config.js";
|
|
3
|
+
import { AssistantMessage, ThinkingBlock, UserMessage } from "./components.js";
|
|
4
|
+
import { loadDisplayResolver, type ToolResultMode } from "./display-config.js";
|
|
5
|
+
import { isRenderModel, mountCall, mountResult, type RenderModel } from "./schema.js";
|
|
10
6
|
|
|
11
7
|
export interface RenderState {
|
|
12
8
|
state: Record<string, unknown>;
|
|
@@ -14,41 +10,19 @@ export interface RenderState {
|
|
|
14
10
|
}
|
|
15
11
|
|
|
16
12
|
export interface UserMessageArgs extends RenderState { text: string }
|
|
17
|
-
|
|
18
13
|
export interface AssistantArgs extends RenderState { text: string }
|
|
19
|
-
|
|
20
14
|
export interface ThinkingArgs extends RenderState { text: string; hidden: boolean }
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
title: string;
|
|
26
|
-
kind?: string;
|
|
27
|
-
displayDetail?: string;
|
|
28
|
-
rawInput?: unknown;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ToolResultArgs extends RenderState {
|
|
32
|
-
toolCallId: string;
|
|
33
|
-
name: string;
|
|
34
|
-
kind?: string;
|
|
35
|
-
rawInput?: unknown;
|
|
36
|
-
/** Resolved from ashi.display.{name} (or .default) in settings.json. */
|
|
37
|
-
mode: ToolResultMode;
|
|
38
|
-
previewLines: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Mutated by ashi when the tool completes. Renderers may ignore setStatus
|
|
42
|
-
* if they encode status differently (e.g. a sigil in the call line). */
|
|
16
|
+
/** The contract ashi's frontend mutates on the call-side component. Schema
|
|
17
|
+
* renderers satisfy this via SchemaCallComponent — extension authors don't
|
|
18
|
+
* implement it directly. */
|
|
43
19
|
export interface ToolCallView extends Component {
|
|
44
20
|
setStatus(opts: { exitCode: number | null; elapsedMs: number; summary?: string }): void;
|
|
21
|
+
toggleExpanded?(): void;
|
|
45
22
|
}
|
|
46
23
|
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
* show diffs. The renderer is called on each terminal-width change so diffs
|
|
50
|
-
* reflow on resize. toggleExpanded flips the view's internal expansion state
|
|
51
|
-
* (Ctrl+O). */
|
|
24
|
+
/** Result-side counterpart. setDiffRenderer receives the width-aware diff
|
|
25
|
+
* closure produced by the edit/write tool at finalize. */
|
|
52
26
|
export interface ToolResultView extends Component {
|
|
53
27
|
appendChunk(chunk: string): void;
|
|
54
28
|
setDiffRenderer(fn: (width: number) => string[]): void;
|
|
@@ -56,12 +30,8 @@ export interface ToolResultView extends Component {
|
|
|
56
30
|
toggleExpanded(): void;
|
|
57
31
|
}
|
|
58
32
|
|
|
59
|
-
const
|
|
60
|
-
const RESULT_PREFIX = "ashi:render-tool-result:";
|
|
33
|
+
const SCHEMA_PREFIX = "ashi:render-tool:";
|
|
61
34
|
|
|
62
|
-
/** Register the default render-* handlers. Per-tool overrides are advised by
|
|
63
|
-
* name (e.g. `ashi:render-tool-call:bash`); unknown tools fall back to
|
|
64
|
-
* `:default`. */
|
|
65
35
|
export function registerRenderDefaults(ctx: ExtensionContext): void {
|
|
66
36
|
ctx.define("ashi:render-user-message", (args: UserMessageArgs): Component => {
|
|
67
37
|
return new UserMessage(args.text);
|
|
@@ -85,54 +55,68 @@ export function registerRenderDefaults(ctx: ExtensionContext): void {
|
|
|
85
55
|
tb.setHidden(args.hidden);
|
|
86
56
|
return tb;
|
|
87
57
|
});
|
|
58
|
+
}
|
|
88
59
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
60
|
+
export interface ToolCallResolveArgs {
|
|
61
|
+
toolCallId: string;
|
|
62
|
+
name: string;
|
|
63
|
+
title: string;
|
|
64
|
+
kind?: string;
|
|
65
|
+
displayDetail?: string;
|
|
66
|
+
rawInput?: unknown;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ToolResultResolveArgs {
|
|
70
|
+
toolCallId: string;
|
|
71
|
+
name: string;
|
|
72
|
+
kind?: string;
|
|
73
|
+
rawInput?: unknown;
|
|
92
74
|
}
|
|
93
75
|
|
|
94
76
|
export interface ToolHookResolver {
|
|
95
|
-
call: (args:
|
|
96
|
-
result: (args:
|
|
77
|
+
call: (args: ToolCallResolveArgs) => ToolCallView;
|
|
78
|
+
result: (args: ToolResultResolveArgs) => ToolResultView;
|
|
97
79
|
modeFor: (name: string) => { mode: ToolResultMode; previewLines: number };
|
|
98
80
|
}
|
|
99
81
|
|
|
100
|
-
/** Resolves
|
|
101
|
-
*
|
|
102
|
-
*
|
|
82
|
+
/** Resolves a tool name to a schema RenderModel — :{name} first, then :default.
|
|
83
|
+
* Cache the registered-handler set; callers can `refresh()` after extensions
|
|
84
|
+
* register new tool-specific renderers. */
|
|
103
85
|
export function createToolHookResolver(
|
|
104
86
|
ctx: ExtensionContext,
|
|
105
|
-
renderState: () => RenderState,
|
|
106
87
|
): ToolHookResolver & { refresh: () => void } {
|
|
107
|
-
const
|
|
88
|
+
const resolver = loadDisplayResolver();
|
|
108
89
|
let registered = new Set(ctx.list());
|
|
109
90
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
91
|
+
const schemaModel = (name: string): RenderModel<unknown> => {
|
|
92
|
+
for (const candidate of [`${SCHEMA_PREFIX}${name}`, `${SCHEMA_PREFIX}default`]) {
|
|
93
|
+
if (!registered.has(candidate)) continue;
|
|
94
|
+
const v = ctx.call(candidate, {}) as unknown;
|
|
95
|
+
if (isRenderModel(v)) return v;
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`no render model for tool "${name}" and no ${SCHEMA_PREFIX}default registered`);
|
|
113
98
|
};
|
|
114
99
|
|
|
100
|
+
// SchemaResultComponent.render(width) corrects env.width on the first frame.
|
|
101
|
+
const initialWidth = (): number => process.stdout.columns ?? 80;
|
|
102
|
+
|
|
115
103
|
return {
|
|
116
104
|
refresh(): void {
|
|
117
105
|
registered = new Set(ctx.list());
|
|
118
106
|
},
|
|
119
107
|
modeFor(name: string) {
|
|
120
|
-
const e =
|
|
108
|
+
const e = resolver.resolve(name, schemaModel(name).display);
|
|
121
109
|
return { mode: e.result, previewLines: e.previewLines };
|
|
122
110
|
},
|
|
123
111
|
call(args) {
|
|
124
|
-
const
|
|
125
|
-
return
|
|
112
|
+
const { mode, previewLines } = this.modeFor(args.name);
|
|
113
|
+
return mountCall(schemaModel(args.name), args,
|
|
114
|
+
{ width: initialWidth(), mode, previewLines }) as ToolCallView;
|
|
126
115
|
},
|
|
127
116
|
result(args) {
|
|
128
117
|
const { mode, previewLines } = this.modeFor(args.name);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
...renderState(),
|
|
132
|
-
...args,
|
|
133
|
-
mode,
|
|
134
|
-
previewLines,
|
|
135
|
-
}) as ToolResultView;
|
|
118
|
+
return mountResult(schemaModel(args.name), { ...args, title: args.name },
|
|
119
|
+
{ width: initialWidth(), mode, previewLines }) as ToolResultView;
|
|
136
120
|
},
|
|
137
121
|
};
|
|
138
122
|
}
|
|
@@ -13,21 +13,62 @@ export interface SessionInfo {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** Many sessions per cwd. Each is one .jsonl file under `dir/`. Constructor
|
|
16
|
-
* always opens a fresh session
|
|
17
|
-
*
|
|
16
|
+
* always opens a fresh session unless `opts.resumeSessionId` is given and
|
|
17
|
+
* points to an existing session file. /resume callers can `openSession(id)`
|
|
18
|
+
* to swap the current store to a past session file. */
|
|
18
19
|
export class MultiSessionStore {
|
|
19
20
|
private dir: string;
|
|
20
21
|
private cwd: string;
|
|
21
22
|
private currentStore: SessionStore;
|
|
22
23
|
|
|
23
|
-
constructor(dir: string, cwd: string) {
|
|
24
|
+
constructor(dir: string, cwd: string, opts?: { resumeSessionId?: string }) {
|
|
24
25
|
this.dir = dir;
|
|
25
26
|
this.cwd = cwd;
|
|
26
27
|
fs.mkdirSync(dir, { recursive: true });
|
|
27
28
|
this.migrateLegacy();
|
|
29
|
+
if (opts?.resumeSessionId) {
|
|
30
|
+
const filePath = this.sessionFile(opts.resumeSessionId);
|
|
31
|
+
if (fs.existsSync(filePath)) {
|
|
32
|
+
this.currentStore = new SessionStore(filePath);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
28
36
|
this.currentStore = this.createFreshSession();
|
|
29
37
|
}
|
|
30
38
|
|
|
39
|
+
markLastSession(): void {
|
|
40
|
+
const lastFile = path.join(this.dir, ".last-session");
|
|
41
|
+
fs.writeFileSync(lastFile, this.currentStore.id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static readLastSessionId(dir: string, opts?: { fallbackToLatest?: boolean }): string | undefined {
|
|
45
|
+
const lastFile = path.join(dir, ".last-session");
|
|
46
|
+
try {
|
|
47
|
+
const id = fs.readFileSync(lastFile, "utf-8").trim();
|
|
48
|
+
if (id && fs.existsSync(path.join(dir, `${id}.jsonl`))) return id;
|
|
49
|
+
} catch { /* no .last-session file yet */ }
|
|
50
|
+
|
|
51
|
+
if (opts?.fallbackToLatest) {
|
|
52
|
+
let best: { id: string; createdAt: number } | undefined;
|
|
53
|
+
let names: string[];
|
|
54
|
+
try { names = fs.readdirSync(dir); } catch { return undefined; }
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
57
|
+
const id = name.slice(0, -".jsonl".length);
|
|
58
|
+
try {
|
|
59
|
+
const raw = fs.readFileSync(path.join(dir, `${id}.jsonl.meta`), "utf-8");
|
|
60
|
+
const meta = JSON.parse(raw) as { createdAt?: number };
|
|
61
|
+
if (typeof meta.createdAt === "number" && (!best || meta.createdAt > best.createdAt)) {
|
|
62
|
+
best = { id, createdAt: meta.createdAt };
|
|
63
|
+
}
|
|
64
|
+
} catch { /* skip unreadable meta */ }
|
|
65
|
+
}
|
|
66
|
+
if (best) return best.id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
31
72
|
/** One-time import from the previous storage format (sessions stored as
|
|
32
73
|
* directories with tree.jsonl + snapshots/). Each old session is replayed
|
|
33
74
|
* from its most recent snapshot into a new flat `.jsonl` file, then the
|