tinker-agent 2.8.0 → 2.10.0
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/CHANGELOG.md +79 -1
- package/README.md +81 -11
- package/package.json +5 -3
- package/src/agent/runtime-context-capabilities.ts +19 -0
- package/src/agent/runtime-context-events.ts +127 -0
- package/src/agent/runtime-context-maintenance.ts +780 -0
- package/src/agent/runtime-hosted-session.ts +443 -0
- package/src/agent/runtime-interactions.ts +291 -0
- package/src/agent/runtime-prompt-scheduler.ts +182 -0
- package/src/agent/runtime-session-contracts.ts +317 -0
- package/src/agent/runtime-session.ts +250 -2130
- package/src/agent/runtime-skills.ts +544 -0
- package/src/cli/command-line.ts +26 -2
- package/src/cli/connect-runner.tsx +26 -0
- package/src/cli/main.ts +26 -0
- package/src/cli/output.ts +1 -1
- package/src/cli/public-cli-contract.ts +18 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/runner-dependencies.ts +6 -5
- package/src/cli/serve-runner.ts +45 -0
- package/src/cli/serve-runtime.ts +100 -0
- package/src/context/context-automation-policy.ts +12 -118
- package/src/context/context-swap-renderer.ts +14 -0
- package/src/events/types.ts +12 -0
- package/src/memory/memory-get-tool.ts +1 -1
- package/src/observation/observation-builder.ts +128 -48
- package/src/remote/client.ts +350 -0
- package/src/remote/config.ts +95 -0
- package/src/remote/http-server.ts +240 -0
- package/src/remote/protocol.ts +228 -0
- package/src/remote/service-store.ts +175 -0
- package/src/remote/service.ts +219 -0
- package/src/remote/sync-hub.ts +95 -0
- package/src/session/remote-history-reader.ts +143 -0
- package/src/session/resume-projection.ts +47 -21
- package/src/session/session-history-access.ts +238 -0
- package/src/session/session-store-context-readers.ts +183 -0
- package/src/session/session-store-ledger-writer.ts +315 -0
- package/src/session/session-store-record-writer.ts +318 -0
- package/src/session/session-store-recovery.ts +225 -0
- package/src/session/session-store-revisions.ts +1004 -0
- package/src/session/session-store-sql.ts +40 -0
- package/src/session/session-store-validation.ts +657 -0
- package/src/session/session-store.ts +756 -3186
- package/src/tools/bash-task.ts +46 -18
- package/src/tools/bash.ts +44 -2
- package/src/tools/glob.ts +107 -19
- package/src/tools/grep-output.ts +130 -0
- package/src/tools/grep-pagination.ts +73 -0
- package/src/tools/grep-path.ts +11 -0
- package/src/tools/grep-snippets.ts +111 -0
- package/src/tools/grep.ts +139 -154
- package/src/tools/read.ts +0 -9
- package/src/tools/recall.ts +106 -50
- package/src/tools/registry.ts +4 -6
- package/src/tools/ripgrep.ts +19 -26
- package/src/tools/shell-process.ts +30 -4
- package/src/tools/task-output-range.ts +146 -0
- package/src/tools/task-output-tool.ts +35 -5
- package/src/tools/task-output.ts +35 -0
- package/src/tools/task-stop.ts +2 -1
- package/src/tools/task-tool-args.ts +34 -0
- package/src/tools/terminal-screen.ts +11 -2
- package/src/tools/types.ts +39 -2
- package/src/tui/event-store.ts +23 -5
- package/src/tui/remote-app.tsx +210 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ShellTaskManager } from "./bash-task";
|
|
1
|
+
import type { ShellTaskInspection, ShellTaskManager } from "./bash-task";
|
|
2
2
|
import { throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
3
|
-
import {
|
|
3
|
+
import { parseTaskOutputArgs } from "./task-tool-args";
|
|
4
4
|
import { defineToolExecutor } from "./types";
|
|
5
5
|
import type { TaskOutputRawResult, ToolExecutionContext, ToolExecutor } from "./types";
|
|
6
6
|
|
|
@@ -10,7 +10,8 @@ export function createTaskOutputToolExecutor(options: {
|
|
|
10
10
|
return defineToolExecutor("task_output", {
|
|
11
11
|
definition: {
|
|
12
12
|
name: "TaskOutput",
|
|
13
|
-
description:
|
|
13
|
+
description:
|
|
14
|
+
"Get a shell task's status and output. Defaults to a head/tail log preview or current PTY screen. For non-PTY tasks, offset/limit selects consecutive numbered log lines instead; PTY tasks ignore these parameters. truncated means content within the requested range was shortened by byte limits, not that other log lines exist. When polling a running log, reread its last line because it may still be growing.",
|
|
14
15
|
parameters: {
|
|
15
16
|
type: "object",
|
|
16
17
|
additionalProperties: false,
|
|
@@ -19,6 +20,20 @@ export function createTaskOutputToolExecutor(options: {
|
|
|
19
20
|
type: "string",
|
|
20
21
|
description: "The task ID returned by Bash or TaskList.",
|
|
21
22
|
},
|
|
23
|
+
offset: {
|
|
24
|
+
type: "integer",
|
|
25
|
+
minimum: 1,
|
|
26
|
+
maximum: Number.MAX_SAFE_INTEGER,
|
|
27
|
+
description:
|
|
28
|
+
"1-based starting log line. Supplying offset or limit selects consecutive lines; default offset is 1. Ignored for PTY tasks.",
|
|
29
|
+
},
|
|
30
|
+
limit: {
|
|
31
|
+
type: "integer",
|
|
32
|
+
minimum: 1,
|
|
33
|
+
maximum: Number.MAX_SAFE_INTEGER,
|
|
34
|
+
description:
|
|
35
|
+
"Maximum number of consecutive log lines to read (default 200 in range mode), subject to byte limits. Ignored for PTY tasks.",
|
|
36
|
+
},
|
|
22
37
|
},
|
|
23
38
|
required: ["task_id"],
|
|
24
39
|
},
|
|
@@ -29,12 +44,26 @@ export function createTaskOutputToolExecutor(options: {
|
|
|
29
44
|
context: ToolExecutionContext,
|
|
30
45
|
): Promise<TaskOutputRawResult> {
|
|
31
46
|
throwIfTurnCancelled(context.signal);
|
|
32
|
-
const parsed =
|
|
47
|
+
const parsed = parseTaskOutputArgs(args);
|
|
33
48
|
if (!parsed.ok) {
|
|
34
49
|
return { ok: false, taskId: "", error: parsed.error };
|
|
35
50
|
}
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
let inspection: ShellTaskInspection | undefined;
|
|
53
|
+
try {
|
|
54
|
+
inspection = await options.taskManager.inspectTaskOutput(
|
|
55
|
+
parsed.taskId,
|
|
56
|
+
parsed.range,
|
|
57
|
+
context.signal,
|
|
58
|
+
);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throwIfTurnCancelled(context.signal);
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
taskId: parsed.taskId,
|
|
64
|
+
error: error instanceof Error ? error.message : String(error),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
38
67
|
throwIfTurnCancelled(context.signal);
|
|
39
68
|
if (inspection === undefined) {
|
|
40
69
|
return {
|
|
@@ -55,6 +84,7 @@ export function createTaskOutputToolExecutor(options: {
|
|
|
55
84
|
preview: inspection.output.preview,
|
|
56
85
|
truncated: inspection.output.truncated,
|
|
57
86
|
omittedLines: inspection.output.omittedLines,
|
|
87
|
+
range: inspection.output.range,
|
|
58
88
|
outputFilePath: inspection.task.outputFilePath,
|
|
59
89
|
screenRows: inspection.screenRows,
|
|
60
90
|
screenColumns: inspection.screenColumns,
|
package/src/tools/task-output.ts
CHANGED
|
@@ -9,7 +9,14 @@ import {
|
|
|
9
9
|
type OutputPreviewSource,
|
|
10
10
|
} from "./bounded-output-preview";
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
readTaskOutputRange,
|
|
14
|
+
type TaskOutputRange,
|
|
15
|
+
type TaskOutputRangeRequest,
|
|
16
|
+
} from "./task-output-range";
|
|
17
|
+
|
|
12
18
|
export type TaskOutputSnapshot = {
|
|
19
|
+
range?: TaskOutputRange;
|
|
13
20
|
outputBytes: number;
|
|
14
21
|
outputLines: number;
|
|
15
22
|
preview: string;
|
|
@@ -48,6 +55,34 @@ export class TaskOutput {
|
|
|
48
55
|
return this.endPromise;
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
async readRange(
|
|
59
|
+
range: TaskOutputRangeRequest,
|
|
60
|
+
signal?: AbortSignal,
|
|
61
|
+
): Promise<TaskOutputSnapshot> {
|
|
62
|
+
const ended = this.endPromise !== undefined;
|
|
63
|
+
if (ended) {
|
|
64
|
+
await this.endPromise;
|
|
65
|
+
}
|
|
66
|
+
const snapshot = this.snapshot();
|
|
67
|
+
if (!ended) {
|
|
68
|
+
// A write callback is a barrier for all bytes captured above; later writes
|
|
69
|
+
// may proceed, but the reader remains bounded to snapshot.outputBytes.
|
|
70
|
+
await new Promise<void>((resolve, reject) => {
|
|
71
|
+
this.stream.write(Buffer.alloc(0), (error) => {
|
|
72
|
+
if (error) reject(error);
|
|
73
|
+
else resolve();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return readTaskOutputRange({
|
|
78
|
+
filePath: this.filePath,
|
|
79
|
+
snapshot,
|
|
80
|
+
range,
|
|
81
|
+
ended,
|
|
82
|
+
signal,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
51
86
|
private async finish(): Promise<TaskOutputSnapshot> {
|
|
52
87
|
this.appendText(this.decoder.end());
|
|
53
88
|
if (this.pendingLine !== "") {
|
package/src/tools/task-stop.ts
CHANGED
|
@@ -37,12 +37,13 @@ export function createTaskStopToolExecutor(options: {
|
|
|
37
37
|
try {
|
|
38
38
|
const result = await options.taskManager.stopTask(parsed.taskId, "tool");
|
|
39
39
|
return {
|
|
40
|
-
ok:
|
|
40
|
+
ok: result.task.error === undefined,
|
|
41
41
|
taskId: parsed.taskId,
|
|
42
42
|
task: result.task,
|
|
43
43
|
status: result.task.status,
|
|
44
44
|
requestedSignal: result.requestedSignal,
|
|
45
45
|
escalated: result.escalated,
|
|
46
|
+
...(result.task.error === undefined ? {} : { error: result.task.error }),
|
|
46
47
|
};
|
|
47
48
|
} catch (error) {
|
|
48
49
|
const inspection = options.taskManager.inspectTask(parsed.taskId);
|
|
@@ -24,6 +24,40 @@ export function parseTaskIdArgs(
|
|
|
24
24
|
return { ok: true, taskId: args.task_id };
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export function parseTaskOutputArgs(args: unknown):
|
|
28
|
+
| {
|
|
29
|
+
ok: true;
|
|
30
|
+
taskId: string;
|
|
31
|
+
range?: { offset: number; limit: number };
|
|
32
|
+
}
|
|
33
|
+
| { ok: false; error: string } {
|
|
34
|
+
if (!isRecord(args)) {
|
|
35
|
+
return { ok: false, error: "TaskOutput arguments must be an object." };
|
|
36
|
+
}
|
|
37
|
+
const { offset, limit, ...rest } = args;
|
|
38
|
+
const parsed = parseTaskIdArgs(rest, "TaskOutput");
|
|
39
|
+
if (!parsed.ok) return parsed;
|
|
40
|
+
for (const [name, value] of Object.entries({ offset, limit })) {
|
|
41
|
+
if (
|
|
42
|
+
value !== undefined &&
|
|
43
|
+
(!Number.isSafeInteger(value) || (value as number) < 1)
|
|
44
|
+
) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: `TaskOutput.${name} must be a positive safe integer.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (offset === undefined && limit === undefined) return parsed;
|
|
52
|
+
return {
|
|
53
|
+
...parsed,
|
|
54
|
+
range: {
|
|
55
|
+
offset: (offset as number | undefined) ?? 1,
|
|
56
|
+
limit: (limit as number | undefined) ?? 200,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
27
61
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
28
62
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
63
|
}
|
|
@@ -3,16 +3,25 @@ import { Terminal } from "@xterm/headless";
|
|
|
3
3
|
|
|
4
4
|
export const TERMINAL_SCREEN_ROWS = 24;
|
|
5
5
|
export const TERMINAL_SCREEN_COLUMNS = 80;
|
|
6
|
+
export const MIN_TERMINAL_COLUMNS = 2;
|
|
7
|
+
export const MAX_TERMINAL_DIMENSION = 1_000;
|
|
6
8
|
|
|
7
9
|
export type TerminalScreen = {
|
|
10
|
+
readonly rows: number;
|
|
11
|
+
readonly columns: number;
|
|
8
12
|
write(bytes: Uint8Array): Promise<void>;
|
|
9
13
|
flush(): Promise<void>;
|
|
10
14
|
text(): string;
|
|
11
15
|
dispose(): void;
|
|
12
16
|
};
|
|
13
17
|
|
|
14
|
-
export function createTerminalScreen(
|
|
15
|
-
|
|
18
|
+
export function createTerminalScreen(
|
|
19
|
+
options: { cols?: number; rows?: number } = {},
|
|
20
|
+
): TerminalScreen {
|
|
21
|
+
return new HeadlessTerminalScreen(
|
|
22
|
+
options.rows ?? TERMINAL_SCREEN_ROWS,
|
|
23
|
+
options.cols ?? TERMINAL_SCREEN_COLUMNS,
|
|
24
|
+
);
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export class HeadlessTerminalScreen implements TerminalScreen {
|
package/src/tools/types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from "../session/session-history-reader";
|
|
10
10
|
import type { ShellTaskSnapshot, ShellTaskStatus } from "./bash-task";
|
|
11
11
|
import type { SkillScope } from "../skills/skill-loader";
|
|
12
|
+
import type { TaskOutputRange } from "./task-output-range";
|
|
12
13
|
|
|
13
14
|
export type JsonSchema = Record<string, unknown>;
|
|
14
15
|
|
|
@@ -91,16 +92,27 @@ export type DeleteFileRawResult = {
|
|
|
91
92
|
|
|
92
93
|
export type GlobRawResult = {
|
|
93
94
|
ok: boolean;
|
|
94
|
-
pattern
|
|
95
|
+
pattern?: string;
|
|
95
96
|
searchPath: string;
|
|
96
97
|
absoluteSearchPath?: string;
|
|
97
98
|
matches?: string[];
|
|
99
|
+
/** Number of paths returned, including in legacy unpaginated results. */
|
|
98
100
|
matchCount?: number;
|
|
101
|
+
/** Exact total before pagination; absent in legacy results. */
|
|
102
|
+
totalMatches?: number;
|
|
103
|
+
returnedCount?: number;
|
|
104
|
+
appliedOffset?: number;
|
|
105
|
+
hasMore?: boolean;
|
|
106
|
+
nextOffset?: number;
|
|
99
107
|
ignored?: string[];
|
|
100
108
|
error?: string;
|
|
101
109
|
};
|
|
102
110
|
|
|
103
|
-
export type GrepOutputMode =
|
|
111
|
+
export type GrepOutputMode =
|
|
112
|
+
| "content"
|
|
113
|
+
| "files_with_matches"
|
|
114
|
+
| "count"
|
|
115
|
+
| "count-matches";
|
|
104
116
|
|
|
105
117
|
export type GrepRawResult = {
|
|
106
118
|
ok: boolean;
|
|
@@ -112,10 +124,27 @@ export type GrepRawResult = {
|
|
|
112
124
|
numFiles: number;
|
|
113
125
|
content?: string;
|
|
114
126
|
numLines?: number;
|
|
127
|
+
/** Structured count records; paths are unescaped and never parsed from display text. */
|
|
128
|
+
counts?: { filePath: string; count: number }[];
|
|
129
|
+
/** Sum on this page: matching lines for count, individual matches for count-matches. */
|
|
115
130
|
numMatches?: number;
|
|
131
|
+
/** Collected pagination units; legacy content results counted all output lines. Not a global total if searchIncomplete. */
|
|
132
|
+
totalResults?: number;
|
|
133
|
+
/** Selected pagination units, excluding context and nearby matches shown as context. */
|
|
134
|
+
returnedResults?: number;
|
|
135
|
+
paginationUnit?: "matching_lines" | "match_events" | "files";
|
|
136
|
+
/** More collected pagination units are available, independently of search completeness. */
|
|
137
|
+
hasMore?: boolean;
|
|
138
|
+
nextOffset?: number;
|
|
116
139
|
appliedLimit?: number;
|
|
117
140
|
appliedOffset?: number;
|
|
141
|
+
/** True for interrupted searches (e.g. timeout or output buffer limit), not pagination. */
|
|
142
|
+
searchIncomplete?: boolean;
|
|
143
|
+
/** The search stopped early, so requested context cannot be guaranteed complete. */
|
|
144
|
+
contextMayBeIncomplete?: boolean;
|
|
145
|
+
/** Legacy default exclusions, not observed skips. New Grep results omit this field. */
|
|
118
146
|
ignored?: string[];
|
|
147
|
+
/** Compatibility flag combining pagination and interrupted search; prefer explicit fields above. */
|
|
119
148
|
truncated?: boolean;
|
|
120
149
|
error?: string;
|
|
121
150
|
};
|
|
@@ -173,6 +202,7 @@ export type UpdatePlanRawResult =
|
|
|
173
202
|
};
|
|
174
203
|
|
|
175
204
|
export type TaskOutputRawResult = {
|
|
205
|
+
range?: TaskOutputRange;
|
|
176
206
|
ok: boolean;
|
|
177
207
|
taskId: string;
|
|
178
208
|
task?: ShellTaskSnapshot;
|
|
@@ -273,6 +303,7 @@ export type GenericToolRawResult = {
|
|
|
273
303
|
};
|
|
274
304
|
|
|
275
305
|
export type RecallToolErrorCode =
|
|
306
|
+
| import("../session/session-history-access").RecallSessionErrorCode
|
|
276
307
|
| "RECALL_ARGS_INVALID"
|
|
277
308
|
| "RECALL_SOURCE_INVALID"
|
|
278
309
|
| "RECALL_SOURCE_NOT_FOUND"
|
|
@@ -284,6 +315,9 @@ export type RecallSearchRawResult =
|
|
|
284
315
|
ok: true;
|
|
285
316
|
mode: "search";
|
|
286
317
|
historical: true;
|
|
318
|
+
/** Optional only for persisted results produced before session selection. */
|
|
319
|
+
sessionId?: SessionId;
|
|
320
|
+
workspaceRoot?: string;
|
|
287
321
|
query: string;
|
|
288
322
|
filters: RecallSearchFilters;
|
|
289
323
|
page: RecallSearchPage;
|
|
@@ -300,6 +334,9 @@ export type RecallGetRawResult =
|
|
|
300
334
|
ok: true;
|
|
301
335
|
mode: "get";
|
|
302
336
|
historical: true;
|
|
337
|
+
/** Optional only for persisted results produced before session selection. */
|
|
338
|
+
sessionId?: SessionId;
|
|
339
|
+
workspaceRoot?: string;
|
|
303
340
|
page: RecallGetPage;
|
|
304
341
|
}
|
|
305
342
|
| {
|
package/src/tui/event-store.ts
CHANGED
|
@@ -873,6 +873,9 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
873
873
|
? base
|
|
874
874
|
: `${base} -> ${raw.asset.mimeType}, ${raw.asset.width}x${raw.asset.height}, ${raw.asset.byteLength} bytes`;
|
|
875
875
|
case "glob":
|
|
876
|
+
if (raw.ok && raw.totalMatches !== undefined) {
|
|
877
|
+
return `${base} -> ${raw.returnedCount ?? raw.matches?.length ?? 0} of ${raw.totalMatches} matches`;
|
|
878
|
+
}
|
|
876
879
|
return raw.ok && raw.matchCount !== undefined
|
|
877
880
|
? `${base} -> ${raw.matchCount} match${raw.matchCount === 1 ? "" : "es"}`
|
|
878
881
|
: base;
|
|
@@ -881,11 +884,21 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
881
884
|
return `${base} -> ${raw.numLines} line${raw.numLines === 1 ? "" : "s"}`;
|
|
882
885
|
}
|
|
883
886
|
if (
|
|
884
|
-
raw.mode === "count" &&
|
|
887
|
+
(raw.mode === "count" || raw.mode === "count-matches") &&
|
|
885
888
|
raw.numMatches !== undefined &&
|
|
886
889
|
raw.numFiles !== undefined
|
|
887
890
|
) {
|
|
888
|
-
|
|
891
|
+
const unit =
|
|
892
|
+
raw.mode === "count"
|
|
893
|
+
? `matching line${raw.numMatches === 1 ? "" : "s"}`
|
|
894
|
+
: `match${raw.numMatches === 1 ? "" : "es"}`;
|
|
895
|
+
const scope =
|
|
896
|
+
raw.appliedLimit !== undefined || (raw.appliedOffset ?? 0) > 0
|
|
897
|
+
? " (this page)"
|
|
898
|
+
: raw.truncated
|
|
899
|
+
? " (partial results)"
|
|
900
|
+
: "";
|
|
901
|
+
return `${base} -> ${raw.numMatches} ${unit} across ${raw.numFiles} file${raw.numFiles === 1 ? "" : "s"}${scope}`;
|
|
889
902
|
}
|
|
890
903
|
return `${base} -> ${raw.numFiles} file${raw.numFiles === 1 ? "" : "s"}`;
|
|
891
904
|
case "read":
|
|
@@ -922,13 +935,18 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
922
935
|
return raw.route === undefined
|
|
923
936
|
? base
|
|
924
937
|
: `${base} -> ok (${raw.route}${raw.refined === true ? ", refined" : ""})`;
|
|
925
|
-
case "recall":
|
|
938
|
+
case "recall": {
|
|
926
939
|
if (!raw.ok) {
|
|
927
940
|
return base;
|
|
928
941
|
}
|
|
942
|
+
const provenance =
|
|
943
|
+
raw.sessionId === undefined
|
|
944
|
+
? ""
|
|
945
|
+
: ` [session=${raw.sessionId}, workspace=${raw.workspaceRoot ?? "unknown"}]`;
|
|
929
946
|
return raw.mode === "search"
|
|
930
|
-
? `${base} -> ${raw.page.hits.length} historical match${raw.page.hits.length === 1 ? "" : "es"}`
|
|
931
|
-
: `${base} -> ${raw.page.returnedBytes} historical bytes`;
|
|
947
|
+
? `${base} -> ${raw.page.hits.length} historical match${raw.page.hits.length === 1 ? "" : "es"}${provenance}`
|
|
948
|
+
: `${base} -> ${raw.page.returnedBytes} historical bytes${provenance}`;
|
|
949
|
+
}
|
|
932
950
|
case "context_maintenance":
|
|
933
951
|
if (!raw.ok) {
|
|
934
952
|
return raw.operation === "swap"
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
2
|
+
import { TextInput } from "@inkjs/ui";
|
|
3
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { RemoteClient } from "../remote/client";
|
|
5
|
+
import type { RemoteSessionInfo } from "../remote/protocol";
|
|
6
|
+
|
|
7
|
+
/** An explicit network client mode; the normal App/controller path is untouched. */
|
|
8
|
+
export function RemoteApp({ client }: { client: RemoteClient }) {
|
|
9
|
+
const state = useSyncExternalStore(client.subscribe, client.getSnapshot);
|
|
10
|
+
const { exit } = useApp();
|
|
11
|
+
const [workspaces, setWorkspaces] = useState<{ id: string; name: string }[]>([]);
|
|
12
|
+
const [sessions, setSessions] = useState<RemoteSessionInfo[]>([]);
|
|
13
|
+
const [workspace, setWorkspace] = useState<string | undefined>(client.workspaceId);
|
|
14
|
+
const [notice, setNotice] = useState(
|
|
15
|
+
"Select a workspace number. /workspaces returns here; /quit detaches.",
|
|
16
|
+
);
|
|
17
|
+
const [inputKey, setInputKey] = useState(0);
|
|
18
|
+
const [visibleCount, setVisibleCount] = useState(30);
|
|
19
|
+
const [browsing, setBrowsing] = useState(!client.sessionId);
|
|
20
|
+
const report = (error: unknown) =>
|
|
21
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
void client
|
|
24
|
+
.workspaces()
|
|
25
|
+
.then((result) => setWorkspaces(result.workspaces))
|
|
26
|
+
.catch(report);
|
|
27
|
+
}, [client]);
|
|
28
|
+
useInput((input, key) => {
|
|
29
|
+
if (key.ctrl && input === "c") exit();
|
|
30
|
+
});
|
|
31
|
+
const submit = async (text: string) => {
|
|
32
|
+
setInputKey((value) => value + 1);
|
|
33
|
+
if (!text.trim()) return;
|
|
34
|
+
if (text === "/quit") {
|
|
35
|
+
exit();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (text === "/workspaces") {
|
|
39
|
+
setWorkspace(undefined);
|
|
40
|
+
setBrowsing(true);
|
|
41
|
+
setSessions([]);
|
|
42
|
+
setWorkspaces((await client.workspaces()).workspaces);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (browsing) {
|
|
46
|
+
if (!workspace) {
|
|
47
|
+
const chosen = workspaces[Number(text) - 1];
|
|
48
|
+
if (!chosen) throw new Error("Enter a workspace number.");
|
|
49
|
+
setWorkspace(chosen.id);
|
|
50
|
+
setSessions((await client.sessions(chosen.id)).sessions);
|
|
51
|
+
setNotice(
|
|
52
|
+
"Enter a session number, or /new. A local session is explicitly attached when selected.",
|
|
53
|
+
);
|
|
54
|
+
} else if (text === "/new") {
|
|
55
|
+
await client.submit({ kind: "create", workspaceId: workspace });
|
|
56
|
+
setBrowsing(false);
|
|
57
|
+
} else {
|
|
58
|
+
const chosen = sessions[Number(text) - 1];
|
|
59
|
+
if (!chosen) throw new Error("Enter a session number or /new.");
|
|
60
|
+
if (chosen.owner === "local")
|
|
61
|
+
await client.submit({
|
|
62
|
+
kind: "adopt",
|
|
63
|
+
workspaceId: workspace,
|
|
64
|
+
sessionId: chosen.id,
|
|
65
|
+
});
|
|
66
|
+
else await client.select(chosen.id, workspace);
|
|
67
|
+
setBrowsing(false);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const sessionId = client.sessionId;
|
|
72
|
+
if (!sessionId) throw new Error("Waiting for the service to accept the session.");
|
|
73
|
+
const pending = state.view?.interaction;
|
|
74
|
+
if (text === "/stop") {
|
|
75
|
+
if (!state.view?.activeRequestId) throw new Error("No active task is shown.");
|
|
76
|
+
await client.submit({
|
|
77
|
+
kind: "stop",
|
|
78
|
+
sessionId,
|
|
79
|
+
targetRequestId: state.view.activeRequestId,
|
|
80
|
+
});
|
|
81
|
+
} else if (text === "/allow" || text === "/deny") {
|
|
82
|
+
if (pending?.kind !== "confirmation")
|
|
83
|
+
throw new Error("No confirmation is pending.");
|
|
84
|
+
await client.submit({
|
|
85
|
+
kind: "confirm",
|
|
86
|
+
sessionId,
|
|
87
|
+
interactionId: pending.id,
|
|
88
|
+
decision: text === "/allow" ? "allow" : "deny",
|
|
89
|
+
});
|
|
90
|
+
} else if (text.startsWith("/answer ") || text === "/dismiss") {
|
|
91
|
+
if (pending?.kind !== "question") throw new Error("No question is pending.");
|
|
92
|
+
await client.submit({
|
|
93
|
+
kind: "answer",
|
|
94
|
+
sessionId,
|
|
95
|
+
interactionId: pending.id,
|
|
96
|
+
selectedIndex: text === "/dismiss" ? null : Number(text.slice(8)) - 1,
|
|
97
|
+
});
|
|
98
|
+
} else if (text === "/history") {
|
|
99
|
+
await client.loadOlderHistory();
|
|
100
|
+
setVisibleCount((count) => count + 80);
|
|
101
|
+
setNotice(
|
|
102
|
+
"Older history loaded; use your terminal scrollback to read previous renders.",
|
|
103
|
+
);
|
|
104
|
+
} else if (text.startsWith("/")) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Commands: /stop /allow /deny /answer N /dismiss /history /workspaces /quit",
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
await client.submit({ kind: "prompt", sessionId, prompt: text });
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const view = state.view;
|
|
113
|
+
return (
|
|
114
|
+
<Box flexDirection="column">
|
|
115
|
+
<Text bold color="cyan">
|
|
116
|
+
Tinker · Service connection
|
|
117
|
+
</Text>
|
|
118
|
+
<Text>
|
|
119
|
+
{state.connection} · {view?.status ?? "select session"} · {state.pending}{" "}
|
|
120
|
+
unsubmitted
|
|
121
|
+
</Text>
|
|
122
|
+
<Text dimColor>{notice}</Text>
|
|
123
|
+
{state.error && <Text color="red">{safe(state.error)}</Text>}
|
|
124
|
+
{browsing ? (
|
|
125
|
+
<Box flexDirection="column">
|
|
126
|
+
{!workspace
|
|
127
|
+
? workspaces.map((w, i) => (
|
|
128
|
+
<Text key={w.id}>
|
|
129
|
+
{i + 1}. {safe(w.name)}
|
|
130
|
+
</Text>
|
|
131
|
+
))
|
|
132
|
+
: sessions.map((s, i) => (
|
|
133
|
+
<Text key={s.id}>
|
|
134
|
+
{i + 1}. {safe(s.title)} · {s.status} · {s.owner}
|
|
135
|
+
</Text>
|
|
136
|
+
))}
|
|
137
|
+
</Box>
|
|
138
|
+
) : (
|
|
139
|
+
<Box flexDirection="column">
|
|
140
|
+
<Text bold>{safe(view?.session.title ?? "Connecting…")}</Text>
|
|
141
|
+
{view?.history.messages.slice(-visibleCount).map((message) => (
|
|
142
|
+
<Box key={message.id} flexDirection="column" marginTop={1}>
|
|
143
|
+
<Text bold color={message.role === "user" ? "green" : "cyan"}>
|
|
144
|
+
{message.name ?? message.role}
|
|
145
|
+
</Text>
|
|
146
|
+
<Text>{safe(message.text)}</Text>
|
|
147
|
+
{message.toolCalls?.map((call) => (
|
|
148
|
+
<Text key={call.id} dimColor>
|
|
149
|
+
{call.name} {safe(call.arguments)}
|
|
150
|
+
</Text>
|
|
151
|
+
))}
|
|
152
|
+
</Box>
|
|
153
|
+
))}
|
|
154
|
+
{view?.streaming && (
|
|
155
|
+
<Box flexDirection="column">
|
|
156
|
+
<Text dimColor>Generating…</Text>
|
|
157
|
+
<Text>{safe(view.streaming.text)}</Text>
|
|
158
|
+
</Box>
|
|
159
|
+
)}
|
|
160
|
+
{view?.tools
|
|
161
|
+
.filter((tool) => tool.status === "running")
|
|
162
|
+
.map((tool) => (
|
|
163
|
+
<Text key={tool.id} color="yellow">
|
|
164
|
+
{tool.name}: {safe(tool.arguments)}
|
|
165
|
+
</Text>
|
|
166
|
+
))}
|
|
167
|
+
{view?.interaction?.kind === "question" && (
|
|
168
|
+
<Box flexDirection="column">
|
|
169
|
+
<Text color="yellow">{safe(view.interaction.question)}</Text>
|
|
170
|
+
{view.interaction.options.map((option, i) => (
|
|
171
|
+
<Text key={i}>
|
|
172
|
+
{i + 1}. {safe(option.description)}
|
|
173
|
+
</Text>
|
|
174
|
+
))}
|
|
175
|
+
<Text>/answer N or /dismiss</Text>
|
|
176
|
+
</Box>
|
|
177
|
+
)}
|
|
178
|
+
{view?.interaction?.kind === "confirmation" && (
|
|
179
|
+
<Box flexDirection="column">
|
|
180
|
+
<Text color="yellow">{safe(view.interaction.command)}</Text>
|
|
181
|
+
<Text>{safe(view.interaction.reason)} · /allow or /deny</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
)}
|
|
184
|
+
</Box>
|
|
185
|
+
)}
|
|
186
|
+
<Box marginTop={1}>
|
|
187
|
+
<Text color="cyan">› </Text>
|
|
188
|
+
<TextInput
|
|
189
|
+
key={inputKey}
|
|
190
|
+
placeholder={
|
|
191
|
+
browsing
|
|
192
|
+
? "Choose a workspace/session"
|
|
193
|
+
: "Send task; /stop cancels; /quit detaches"
|
|
194
|
+
}
|
|
195
|
+
onSubmit={(value) => {
|
|
196
|
+
void submit(value).catch(report);
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
</Box>
|
|
200
|
+
</Box>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
function safe(value: string): string {
|
|
204
|
+
return [...value]
|
|
205
|
+
.filter((character) => {
|
|
206
|
+
const code = character.codePointAt(0)!;
|
|
207
|
+
return code === 10 || code === 9 || (code >= 32 && (code < 127 || code > 159));
|
|
208
|
+
})
|
|
209
|
+
.join("");
|
|
210
|
+
}
|