@blackbelt-technology/pi-agent-dashboard 0.4.5 → 0.4.6
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/AGENTS.md +10 -84
- package/README.md +20 -2
- package/docs/architecture.md +28 -2
- package/package.json +4 -4
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/prompt-bus.test.ts +44 -0
- package/packages/extension/src/__tests__/vcs-info-jj.test.ts +145 -0
- package/packages/extension/src/__tests__/{git-info.test.ts → vcs-info.test.ts} +6 -6
- package/packages/extension/src/bridge-context.ts +7 -0
- package/packages/extension/src/bridge.ts +32 -3
- package/packages/extension/src/model-tracker.ts +35 -1
- package/packages/extension/src/prompt-bus.ts +4 -3
- package/packages/extension/src/session-sync.ts +1 -1
- package/packages/extension/src/vcs-info.ts +184 -0
- package/packages/server/package.json +4 -4
- package/packages/server/src/__tests__/is-unread-trigger.test.ts +4 -2
- package/packages/server/src/__tests__/jj-routes.test.ts +93 -0
- package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +114 -0
- package/packages/server/src/__tests__/session-diff-vcs.test.ts +61 -0
- package/packages/server/src/__tests__/system-routes-restart.test.ts +4 -4
- package/packages/server/src/cli.ts +1 -0
- package/packages/server/src/event-wiring.ts +9 -0
- package/packages/server/src/openspec-tasks.ts +50 -19
- package/packages/server/src/routes/jj-routes.ts +386 -0
- package/packages/server/src/routes/session-routes.ts +12 -3
- package/packages/server/src/server.ts +8 -2
- package/packages/server/src/session-diff.ts +118 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/platform-jj.test.ts +339 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +18 -2
- package/packages/shared/src/config.ts +14 -0
- package/packages/shared/src/diff-types.ts +17 -0
- package/packages/shared/src/platform/jj.ts +405 -0
- package/packages/shared/src/protocol.ts +14 -0
- package/packages/shared/src/tool-registry/definitions.ts +1 -0
- package/packages/shared/src/types.ts +34 -0
- package/packages/extension/src/git-info.ts +0 -55
|
@@ -52,6 +52,7 @@ import { registerPiCoreRoutes } from "./routes/pi-core-routes.js";
|
|
|
52
52
|
import { PiCoreChecker } from "./pi-core-checker.js";
|
|
53
53
|
import { PiCoreUpdater } from "./pi-core-updater.js";
|
|
54
54
|
import { registerToolRoutes } from "./routes/tool-routes.js";
|
|
55
|
+
import { registerJjRoutes } from "./routes/jj-routes.js";
|
|
55
56
|
import { registerBootstrapRoutes } from "./routes/bootstrap-routes.js";
|
|
56
57
|
import { createBootstrapState, type BootstrapStateStore } from "./bootstrap-state.js";
|
|
57
58
|
import { createBootstrapQueue } from "./bootstrap-queue.js";
|
|
@@ -90,6 +91,10 @@ export interface ServerConfig {
|
|
|
90
91
|
editor: import("@blackbelt-technology/pi-dashboard-shared/config.js").EditorConfig;
|
|
91
92
|
/** OpenSpec polling config (interval, concurrency, change detection, jitter) */
|
|
92
93
|
openspec?: import("@blackbelt-technology/pi-dashboard-shared/config.js").OpenSpecPollConfig;
|
|
94
|
+
/** Reattach-placement policy applied when a bridge re-registers after
|
|
95
|
+
* a dashboard restart. Defaults to `"always"`.
|
|
96
|
+
* See change: reattach-move-to-front. */
|
|
97
|
+
reattachPlacement?: import("@blackbelt-technology/pi-dashboard-shared/config.js").ReattachPlacement;
|
|
93
98
|
/** Merged trusted networks from config */
|
|
94
99
|
resolvedTrustedNetworks?: string[];
|
|
95
100
|
/** CORS allowed origins from config */
|
|
@@ -366,7 +371,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
366
371
|
applyReattachPolicy(
|
|
367
372
|
sessionId,
|
|
368
373
|
session.cwd,
|
|
369
|
-
config.reattachPlacement,
|
|
374
|
+
config.reattachPlacement ?? "always",
|
|
370
375
|
{ sessionManager, sessionOrderManager, browserGateway },
|
|
371
376
|
ctx.priorStatus,
|
|
372
377
|
);
|
|
@@ -416,7 +421,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
416
421
|
applyReattachPolicy(
|
|
417
422
|
sessionId,
|
|
418
423
|
session.cwd,
|
|
419
|
-
config.reattachPlacement,
|
|
424
|
+
config.reattachPlacement ?? "always",
|
|
420
425
|
{ sessionManager, sessionOrderManager, browserGateway },
|
|
421
426
|
ctx.priorStatus,
|
|
422
427
|
);
|
|
@@ -706,6 +711,7 @@ export async function createServer(config: ServerConfig): Promise<DashboardServe
|
|
|
706
711
|
});
|
|
707
712
|
registerSystemRoutes(fastify, { sessionManager, preferencesStore, metaPersistence, config, networkGuard, version: pkgVersion, directoryService, piGateway });
|
|
708
713
|
registerToolRoutes(fastify, { registry: getDefaultRegistry(), networkGuard });
|
|
714
|
+
registerJjRoutes(fastify, { browserGateway, pendingAttachRegistry, networkGuard });
|
|
709
715
|
|
|
710
716
|
// ── Bootstrap REST routes ────────────────────────────────────────
|
|
711
717
|
// The routes module is registered here; state + queue are declared
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import { readFileSync, existsSync } from "node:fs";
|
|
6
6
|
import { resolve, relative, isAbsolute, sep as pathSep } from "node:path";
|
|
7
7
|
import * as git from "@blackbelt-technology/pi-dashboard-shared/platform/git.js";
|
|
8
|
-
import
|
|
8
|
+
import * as jj from "@blackbelt-technology/pi-dashboard-shared/platform/jj.js";
|
|
9
|
+
import type { DashboardEvent, JjState } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
9
10
|
import type { FileChangeEvent, FileDiffEntry, EditOperation } from "@blackbelt-technology/pi-dashboard-shared/diff-types.js";
|
|
10
11
|
import { isGitRepo } from "./git-operations.js";
|
|
11
12
|
|
|
@@ -176,3 +177,119 @@ export function enrichWithGitDiff(
|
|
|
176
177
|
|
|
177
178
|
return { enrichedFiles: enriched, isGitRepo: true };
|
|
178
179
|
}
|
|
180
|
+
|
|
181
|
+
// ── jj enrichment (regime-aware) ─────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Pure helper: pick the right diff base for a given jj state.
|
|
185
|
+
* - default workspace → `@-` (equivalent to `git diff HEAD`)
|
|
186
|
+
* - non-default → `fork_point(@, trunk())`
|
|
187
|
+
*
|
|
188
|
+
* Exported for unit testing without spawning jj.
|
|
189
|
+
*/
|
|
190
|
+
export function selectJjDiffBase(jjState: JjState | undefined): {
|
|
191
|
+
diffBase: string;
|
|
192
|
+
baseLabel: string;
|
|
193
|
+
} {
|
|
194
|
+
const workspace = jjState?.workspaceName;
|
|
195
|
+
if (!workspace || workspace === "default") {
|
|
196
|
+
return { diffBase: "@-", baseLabel: "@-" };
|
|
197
|
+
}
|
|
198
|
+
// Use the `..` range form (always-supported) instead of `fork_point()`
|
|
199
|
+
// (which changed signature across jj versions). `trunk()` returns the
|
|
200
|
+
// most-recent ancestor on main/master/trunk; the diff base is the
|
|
201
|
+
// single tip of trunk so that `--from <base> --to @` materializes the
|
|
202
|
+
// cumulative diff across every agent commit in this workspace.
|
|
203
|
+
return { diffBase: "trunk()", baseLabel: "trunk()" };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Enrich file entries with `jj diff` output, regime-aware. Runs
|
|
208
|
+
* `jj diff --from <baseRev> --to @ -- <path>` per file. Handles new
|
|
209
|
+
* files natively (no synthetic `/dev/null` fallback needed — jj
|
|
210
|
+
* reports new files in unified diff format directly).
|
|
211
|
+
*/
|
|
212
|
+
export function enrichWithJjDiff(
|
|
213
|
+
cwd: string,
|
|
214
|
+
files: FileDiffEntry[],
|
|
215
|
+
jjState: JjState | undefined,
|
|
216
|
+
): { enrichedFiles: FileDiffEntry[]; vcsKind: "jj"; diffBase: string; baseLabel: string } {
|
|
217
|
+
const { diffBase, baseLabel } = selectJjDiffBase(jjState);
|
|
218
|
+
const labelOverride = resolveBaseLabel(cwd, diffBase, baseLabel);
|
|
219
|
+
const enriched = files.map((file) => {
|
|
220
|
+
try {
|
|
221
|
+
const diff = jj.diffOr({
|
|
222
|
+
cwd,
|
|
223
|
+
fromRev: diffBase,
|
|
224
|
+
toRev: "@",
|
|
225
|
+
path: file.path,
|
|
226
|
+
}).trim();
|
|
227
|
+
if (diff) return { ...file, gitDiff: diff };
|
|
228
|
+
return file;
|
|
229
|
+
} catch {
|
|
230
|
+
return file;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
return { enrichedFiles: enriched, vcsKind: "jj", diffBase, baseLabel: labelOverride };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Promote the abstract revset (e.g. `@-` or `fork_point(@, trunk())`) to
|
|
238
|
+
* a human-friendly bookmark name when one exists. Best effort — falls
|
|
239
|
+
* back to the abstract label if jj can't resolve it.
|
|
240
|
+
*/
|
|
241
|
+
function resolveBaseLabel(cwd: string, diffBase: string, fallback: string): string {
|
|
242
|
+
const result = jj.logRevset({
|
|
243
|
+
cwd,
|
|
244
|
+
revset: diffBase,
|
|
245
|
+
template: 'bookmarks ++ "\\n"',
|
|
246
|
+
});
|
|
247
|
+
if (!result.ok) return fallback;
|
|
248
|
+
const first = result.value.trim().split("\n")[0]?.trim();
|
|
249
|
+
if (first && first.length > 0 && first.length < 100) return first;
|
|
250
|
+
return fallback;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Unified dispatcher ──────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
export interface VcsEnrichmentResult {
|
|
256
|
+
enrichedFiles: FileDiffEntry[];
|
|
257
|
+
isGitRepo: boolean;
|
|
258
|
+
vcsKind?: "git" | "jj";
|
|
259
|
+
diffBase?: string;
|
|
260
|
+
baseLabel?: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Regime-aware dispatcher. When the session has `jjState.isJjRepo`,
|
|
265
|
+
* route through `enrichWithJjDiff` (which produces the cumulative diff
|
|
266
|
+
* for non-default workspaces). Otherwise fall back to the existing
|
|
267
|
+
* `enrichWithGitDiff` behavior unchanged — plain-git regime is byte-
|
|
268
|
+
* equivalent to the pre-change response shape (modulo the now-optional
|
|
269
|
+
* `vcsKind` field that older clients ignore).
|
|
270
|
+
*
|
|
271
|
+
* See change: add-jj-workspace-plugin.
|
|
272
|
+
*/
|
|
273
|
+
export function enrichWithVcsDiff(
|
|
274
|
+
cwd: string,
|
|
275
|
+
files: FileDiffEntry[],
|
|
276
|
+
jjState: JjState | undefined,
|
|
277
|
+
): VcsEnrichmentResult {
|
|
278
|
+
if (jjState?.isJjRepo) {
|
|
279
|
+
const result = enrichWithJjDiff(cwd, files, jjState);
|
|
280
|
+
return {
|
|
281
|
+
enrichedFiles: result.enrichedFiles,
|
|
282
|
+
isGitRepo: jjState.isColocated === true,
|
|
283
|
+
vcsKind: "jj",
|
|
284
|
+
diffBase: result.diffBase,
|
|
285
|
+
baseLabel: result.baseLabel,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const result = enrichWithGitDiff(cwd, files);
|
|
289
|
+
return {
|
|
290
|
+
...result,
|
|
291
|
+
vcsKind: result.isGitRepo ? "git" : undefined,
|
|
292
|
+
diffBase: result.isGitRepo ? "HEAD" : undefined,
|
|
293
|
+
baseLabel: result.isGitRepo ? "HEAD" : undefined,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for packages/shared/src/platform/jj.ts — Recipe argv shapes
|
|
3
|
+
* and the pure `parseWorkspaceList` helper.
|
|
4
|
+
*
|
|
5
|
+
* Live integration tests (running real `jj` against a temp repo) are
|
|
6
|
+
* deferred to the integration-test phase; argv shape coverage here
|
|
7
|
+
* catches the most common refactor mistakes without requiring `jj`
|
|
8
|
+
* on the test runner's PATH.
|
|
9
|
+
*
|
|
10
|
+
* See change: add-jj-workspace-plugin.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect } from "vitest";
|
|
13
|
+
import {
|
|
14
|
+
JJ_VERSION,
|
|
15
|
+
JJ_WORKSPACE_ROOT,
|
|
16
|
+
JJ_WORKSPACE_LIST,
|
|
17
|
+
JJ_WORKSPACE_ADD,
|
|
18
|
+
JJ_WORKSPACE_FORGET,
|
|
19
|
+
JJ_BOOKMARK_CREATE,
|
|
20
|
+
JJ_BOOKMARK_LIST,
|
|
21
|
+
JJ_GIT_INIT_COLOCATE,
|
|
22
|
+
JJ_GIT_PUSH,
|
|
23
|
+
JJ_DIFF,
|
|
24
|
+
JJ_RESOLVE_LIST,
|
|
25
|
+
JJ_OP_LOG_HEAD,
|
|
26
|
+
JJ_OP_RESTORE,
|
|
27
|
+
JJ_REBASE,
|
|
28
|
+
JJ_LOG_REVSET,
|
|
29
|
+
JJ_RECIPES,
|
|
30
|
+
parseWorkspaceList,
|
|
31
|
+
findWorkspaceByName,
|
|
32
|
+
} from "../platform/jj.js";
|
|
33
|
+
|
|
34
|
+
// ── Argv shapes ─────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
describe("JJ_VERSION.argv", () => {
|
|
37
|
+
it("is `jj --version`", () => {
|
|
38
|
+
expect(JJ_VERSION.argv({})).toEqual(["jj", "--version"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("parses `jj 0.18.0` into `0.18.0`", () => {
|
|
42
|
+
expect(JJ_VERSION.parse("jj 0.18.0\n", {})).toBe("0.18.0");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("falls back to trimmed string when version regex fails", () => {
|
|
46
|
+
expect(JJ_VERSION.parse("unknown-format\n", {})).toBe("unknown-format");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("JJ_WORKSPACE_ROOT.argv", () => {
|
|
51
|
+
it("is `jj workspace root`", () => {
|
|
52
|
+
expect(JJ_WORKSPACE_ROOT.argv({ cwd: "/tmp" })).toEqual([
|
|
53
|
+
"jj", "workspace", "root",
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("JJ_WORKSPACE_LIST.argv", () => {
|
|
59
|
+
it("includes --no-pager", () => {
|
|
60
|
+
expect(JJ_WORKSPACE_LIST.argv({ cwd: "/tmp" })).toEqual([
|
|
61
|
+
"jj", "workspace", "list", "--no-pager",
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("JJ_WORKSPACE_ADD.argv", () => {
|
|
67
|
+
it("without baseRev", () => {
|
|
68
|
+
expect(JJ_WORKSPACE_ADD.argv({ cwd: "/repo", destPath: "/repo/.shadow/agent-1" })).toEqual([
|
|
69
|
+
"jj", "workspace", "add", "/repo/.shadow/agent-1",
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("with baseRev", () => {
|
|
74
|
+
expect(JJ_WORKSPACE_ADD.argv({
|
|
75
|
+
cwd: "/repo",
|
|
76
|
+
destPath: "/repo/.shadow/agent-1",
|
|
77
|
+
baseRev: "develop",
|
|
78
|
+
})).toEqual([
|
|
79
|
+
"jj", "workspace", "add", "/repo/.shadow/agent-1", "-r", "develop",
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("path with spaces is passed verbatim (argv-array, no shell)", () => {
|
|
84
|
+
expect(JJ_WORKSPACE_ADD.argv({ cwd: "/repo", destPath: "/repo/my workspace" })).toEqual([
|
|
85
|
+
"jj", "workspace", "add", "/repo/my workspace",
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("JJ_WORKSPACE_FORGET.argv", () => {
|
|
91
|
+
it("is `jj workspace forget <name>`", () => {
|
|
92
|
+
expect(JJ_WORKSPACE_FORGET.argv({ cwd: "/repo", name: "agent-1" })).toEqual([
|
|
93
|
+
"jj", "workspace", "forget", "agent-1",
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("JJ_BOOKMARK_CREATE.argv", () => {
|
|
99
|
+
it("is `jj bookmark create <name> -r <rev>`", () => {
|
|
100
|
+
expect(JJ_BOOKMARK_CREATE.argv({ cwd: "/repo", name: "feat", rev: "@" })).toEqual([
|
|
101
|
+
"jj", "bookmark", "create", "feat", "-r", "@",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("JJ_BOOKMARK_LIST.argv", () => {
|
|
107
|
+
it("includes name template and --no-pager", () => {
|
|
108
|
+
const argv = JJ_BOOKMARK_LIST.argv({ cwd: "/repo" });
|
|
109
|
+
expect(argv[0]).toBe("jj");
|
|
110
|
+
expect(argv).toContain("bookmark");
|
|
111
|
+
expect(argv).toContain("list");
|
|
112
|
+
expect(argv).toContain("-T");
|
|
113
|
+
expect(argv).toContain("--no-pager");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("JJ_GIT_INIT_COLOCATE.argv", () => {
|
|
118
|
+
it("is `jj git init --colocate`", () => {
|
|
119
|
+
expect(JJ_GIT_INIT_COLOCATE.argv({ cwd: "/repo" })).toEqual([
|
|
120
|
+
"jj", "git", "init", "--colocate",
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("JJ_GIT_PUSH.argv", () => {
|
|
126
|
+
it("includes --bookmark <name>", () => {
|
|
127
|
+
expect(JJ_GIT_PUSH.argv({ cwd: "/repo", bookmark: "feat/agent-1" })).toEqual([
|
|
128
|
+
"jj", "git", "push", "--bookmark", "feat/agent-1",
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("JJ_DIFF.argv", () => {
|
|
134
|
+
it("default invocation has no --from/--to", () => {
|
|
135
|
+
expect(JJ_DIFF.argv({ cwd: "/repo" })).toEqual([
|
|
136
|
+
"jj", "diff", "--no-pager",
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("with --from and --to", () => {
|
|
141
|
+
expect(JJ_DIFF.argv({ cwd: "/repo", fromRev: "develop", toRev: "@" })).toEqual([
|
|
142
|
+
"jj", "diff", "--no-pager", "--from", "develop", "--to", "@",
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("with path filter", () => {
|
|
147
|
+
expect(JJ_DIFF.argv({
|
|
148
|
+
cwd: "/repo",
|
|
149
|
+
fromRev: "develop",
|
|
150
|
+
toRev: "@",
|
|
151
|
+
path: "src/auth.ts",
|
|
152
|
+
})).toEqual([
|
|
153
|
+
"jj", "diff", "--no-pager",
|
|
154
|
+
"--from", "develop",
|
|
155
|
+
"--to", "@",
|
|
156
|
+
"--", "src/auth.ts",
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("path-only diff (working copy)", () => {
|
|
161
|
+
expect(JJ_DIFF.argv({ cwd: "/repo", path: "src/auth.ts" })).toEqual([
|
|
162
|
+
"jj", "diff", "--no-pager", "--", "src/auth.ts",
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("JJ_RESOLVE_LIST.argv", () => {
|
|
168
|
+
it("is `jj resolve --list`", () => {
|
|
169
|
+
expect(JJ_RESOLVE_LIST.argv({ cwd: "/repo" })).toEqual([
|
|
170
|
+
"jj", "resolve", "--list", "--no-pager",
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("tolerates exit code 1 (no conflicts)", () => {
|
|
175
|
+
expect(JJ_RESOLVE_LIST.tolerate).toContain(1);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("JJ_OP_LOG_HEAD.argv", () => {
|
|
180
|
+
it("includes --limit 1 and id.short() template", () => {
|
|
181
|
+
const argv = JJ_OP_LOG_HEAD.argv({ cwd: "/repo" });
|
|
182
|
+
expect(argv).toContain("op");
|
|
183
|
+
expect(argv).toContain("log");
|
|
184
|
+
expect(argv).toContain("--limit");
|
|
185
|
+
expect(argv).toContain("1");
|
|
186
|
+
expect(argv).toContain("-T");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("parses single-line short id output", () => {
|
|
190
|
+
expect(JJ_OP_LOG_HEAD.parse("abc1234\n", { cwd: "/repo" })).toBe("abc1234");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("returns undefined for empty output", () => {
|
|
194
|
+
expect(JJ_OP_LOG_HEAD.parse("\n", { cwd: "/repo" })).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("JJ_OP_RESTORE.argv", () => {
|
|
199
|
+
it("is `jj op restore <op-id>`", () => {
|
|
200
|
+
expect(JJ_OP_RESTORE.argv({ cwd: "/repo", opId: "abc1234" })).toEqual([
|
|
201
|
+
"jj", "op", "restore", "abc1234",
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("JJ_REBASE.argv", () => {
|
|
207
|
+
it("is `jj rebase -d <dest> -s <src>`", () => {
|
|
208
|
+
expect(JJ_REBASE.argv({ cwd: "/repo", dest: "main", src: "agent-1" })).toEqual([
|
|
209
|
+
"jj", "rebase", "-d", "main", "-s", "agent-1",
|
|
210
|
+
]);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("JJ_LOG_REVSET.argv", () => {
|
|
215
|
+
it("uses default change_id template", () => {
|
|
216
|
+
const argv = JJ_LOG_REVSET.argv({ cwd: "/repo", revset: "trunk()..@" });
|
|
217
|
+
expect(argv).toContain("log");
|
|
218
|
+
expect(argv).toContain("-r");
|
|
219
|
+
expect(argv).toContain("trunk()..@");
|
|
220
|
+
expect(argv).toContain("--no-graph");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("respects custom template", () => {
|
|
224
|
+
const argv = JJ_LOG_REVSET.argv({
|
|
225
|
+
cwd: "/repo",
|
|
226
|
+
revset: "@",
|
|
227
|
+
template: 'description ++ "\\n"',
|
|
228
|
+
});
|
|
229
|
+
expect(argv).toContain('description ++ "\\n"');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("JJ_RECIPES registry", () => {
|
|
234
|
+
it("enumerates all exported recipes", () => {
|
|
235
|
+
const keys = Object.keys(JJ_RECIPES).sort();
|
|
236
|
+
expect(keys).toEqual([
|
|
237
|
+
"JJ_BOOKMARK_CREATE",
|
|
238
|
+
"JJ_BOOKMARK_LIST",
|
|
239
|
+
"JJ_DIFF",
|
|
240
|
+
"JJ_GIT_INIT_COLOCATE",
|
|
241
|
+
"JJ_GIT_PUSH",
|
|
242
|
+
"JJ_LOG_REVSET",
|
|
243
|
+
"JJ_OP_LOG_HEAD",
|
|
244
|
+
"JJ_OP_RESTORE",
|
|
245
|
+
"JJ_REBASE",
|
|
246
|
+
"JJ_RESOLVE_LIST",
|
|
247
|
+
"JJ_VERSION",
|
|
248
|
+
"JJ_WORKSPACE_ADD",
|
|
249
|
+
"JJ_WORKSPACE_FORGET",
|
|
250
|
+
"JJ_WORKSPACE_LIST",
|
|
251
|
+
"JJ_WORKSPACE_ROOT",
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("every recipe has argv and parse functions", () => {
|
|
256
|
+
for (const [name, recipe] of Object.entries(JJ_RECIPES)) {
|
|
257
|
+
expect(typeof recipe.argv, `${name}.argv`).toBe("function");
|
|
258
|
+
expect(typeof recipe.parse, `${name}.parse`).toBe("function");
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("every recipe's argv starts with `jj`", () => {
|
|
263
|
+
for (const [name, recipe] of Object.entries(JJ_RECIPES)) {
|
|
264
|
+
// Use a forgiving input shape — we only care about the binary name.
|
|
265
|
+
const argv = (recipe.argv as (i: any) => readonly string[])({
|
|
266
|
+
cwd: "/tmp",
|
|
267
|
+
destPath: "/x",
|
|
268
|
+
baseRev: "@",
|
|
269
|
+
name: "x",
|
|
270
|
+
rev: "@",
|
|
271
|
+
bookmark: "x",
|
|
272
|
+
opId: "x",
|
|
273
|
+
dest: "x",
|
|
274
|
+
src: "x",
|
|
275
|
+
revset: "@",
|
|
276
|
+
path: "x",
|
|
277
|
+
});
|
|
278
|
+
expect(argv[0], `${name} first arg`).toBe("jj");
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ── parseWorkspaceList ──────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
describe("parseWorkspaceList", () => {
|
|
286
|
+
it("parses standard two-workspace output", () => {
|
|
287
|
+
const out = `default: rxnxoqlk 4f2c1234 (no description set)
|
|
288
|
+
agent-1: tmysxysu 0c4b5678 (empty) (no description set)
|
|
289
|
+
`;
|
|
290
|
+
expect(parseWorkspaceList(out)).toEqual([
|
|
291
|
+
{ name: "default", changeIdShort: "rxnxoqlk", commitIdShort: "4f2c1234" },
|
|
292
|
+
{ name: "agent-1", changeIdShort: "tmysxysu", commitIdShort: "0c4b5678" },
|
|
293
|
+
]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("captures non-default descriptions", () => {
|
|
297
|
+
const out = `default: rxnxoqlk 4f2c1234 work in progress on auth\n`;
|
|
298
|
+
expect(parseWorkspaceList(out)).toEqual([
|
|
299
|
+
{
|
|
300
|
+
name: "default",
|
|
301
|
+
changeIdShort: "rxnxoqlk",
|
|
302
|
+
commitIdShort: "4f2c1234",
|
|
303
|
+
description: "work in progress on auth",
|
|
304
|
+
},
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("ignores blank and malformed lines", () => {
|
|
309
|
+
const out = `\ndefault: rxnxoqlk 4f2c1234 (no description set)\nrandom garbage\n: missing-name 1234 5678\n`;
|
|
310
|
+
const entries = parseWorkspaceList(out);
|
|
311
|
+
expect(entries.map((e) => e.name)).toEqual(["default"]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("yields entry without ids when format is unexpected", () => {
|
|
315
|
+
const out = `weird-name: this is not an id pair\n`;
|
|
316
|
+
const entries = parseWorkspaceList(out);
|
|
317
|
+
expect(entries).toHaveLength(1);
|
|
318
|
+
expect(entries[0]?.name).toBe("weird-name");
|
|
319
|
+
expect(entries[0]?.changeIdShort).toBeUndefined();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("returns empty array for empty input", () => {
|
|
323
|
+
expect(parseWorkspaceList("")).toEqual([]);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("findWorkspaceByName", () => {
|
|
328
|
+
const fixtures = parseWorkspaceList(
|
|
329
|
+
`default: aaaa 1111 (no description set)\nagent-1: bbbb 2222 (no description set)\n`,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
it("returns the matching entry by name", () => {
|
|
333
|
+
expect(findWorkspaceByName(fixtures, "agent-1")?.changeIdShort).toBe("bbbb");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("returns undefined for unknown name", () => {
|
|
337
|
+
expect(findWorkspaceByName(fixtures, "ghost")).toBeUndefined();
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -179,13 +179,29 @@ describe("openspec binary definition", () => {
|
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
describe("registered tool set", () => {
|
|
182
|
-
it("registers pi, pi-coding-agent, openspec, npm, node, git, zrok, wt", () => {
|
|
182
|
+
it("registers pi, pi-coding-agent, openspec, npm, node, git, jj, zrok, wt", () => {
|
|
183
183
|
const r = freshRegistry({});
|
|
184
|
-
for (const name of ["pi", "pi-coding-agent", "openspec", "npm", "node", "git", "zrok", "wt"]) {
|
|
184
|
+
for (const name of ["pi", "pi-coding-agent", "openspec", "npm", "node", "git", "jj", "zrok", "wt"]) {
|
|
185
185
|
expect(r.has(name)).toBe(true);
|
|
186
186
|
}
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
+
it("jj resolves via where when found", () => {
|
|
190
|
+
const r = freshRegistry({
|
|
191
|
+
which: (name) => (name === "jj" ? "/usr/local/bin/jj" : null),
|
|
192
|
+
});
|
|
193
|
+
const res = r.resolve("jj");
|
|
194
|
+
expect(res.ok).toBe(true);
|
|
195
|
+
expect(res.path).toBe("/usr/local/bin/jj");
|
|
196
|
+
expect(res.source).toBe("system");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("jj unavailable returns ok:false without throwing", () => {
|
|
200
|
+
const r = freshRegistry({ which: () => null });
|
|
201
|
+
const res = r.resolve("jj");
|
|
202
|
+
expect(res.ok).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
189
205
|
it("wt resolves via where when found", () => {
|
|
190
206
|
const r = freshRegistry({
|
|
191
207
|
platform: "win32",
|
|
@@ -134,6 +134,13 @@ export interface DashboardConfig {
|
|
|
134
134
|
editor: EditorConfig;
|
|
135
135
|
/** OpenSpec background polling behavior (interval, concurrency, change detection, jitter) */
|
|
136
136
|
openspec: OpenSpecPollConfig;
|
|
137
|
+
/**
|
|
138
|
+
* Timeout for ask_user prompts in seconds.
|
|
139
|
+
* Default: 300 (5 minutes).
|
|
140
|
+
* Set to -1 (or any value <= 0) for no timeout (waits indefinitely).
|
|
141
|
+
* If the key is absent from config.json the default of 300 s applies.
|
|
142
|
+
*/
|
|
143
|
+
askUserPromptTimeoutSeconds: number;
|
|
137
144
|
/** Networks trusted for full access without authentication (CIDR, wildcard, exact IP) */
|
|
138
145
|
trustedNetworks: string[];
|
|
139
146
|
/** Merged trustedNetworks + auth.bypassHosts (deduplicated). Computed at load time. */
|
|
@@ -168,6 +175,9 @@ export interface CorsConfig {
|
|
|
168
175
|
|
|
169
176
|
const VALID_SPAWN_STRATEGIES: SpawnStrategy[] = ["tmux", "headless"];
|
|
170
177
|
|
|
178
|
+
/** Default ask_user prompt timeout: 300 seconds (5 minutes). */
|
|
179
|
+
export const DEFAULT_ASK_USER_PROMPT_TIMEOUT_SECONDS = 300;
|
|
180
|
+
|
|
171
181
|
const DEFAULTS: DashboardConfig = {
|
|
172
182
|
plugins: {},
|
|
173
183
|
port: 8000,
|
|
@@ -187,6 +197,7 @@ const DEFAULTS: DashboardConfig = {
|
|
|
187
197
|
cors: { allowedOrigins: [] },
|
|
188
198
|
electronMode: false,
|
|
189
199
|
knownServers: [],
|
|
200
|
+
askUserPromptTimeoutSeconds: DEFAULT_ASK_USER_PROMPT_TIMEOUT_SECONDS,
|
|
190
201
|
reattachPlacement: DEFAULT_REATTACH_PLACEMENT,
|
|
191
202
|
};
|
|
192
203
|
|
|
@@ -385,6 +396,9 @@ export function loadConfig(): DashboardConfig {
|
|
|
385
396
|
knownServers: parseKnownServers(parsed.knownServers),
|
|
386
397
|
reattachPlacement: parseReattachPlacement(parsed.reattachPlacement),
|
|
387
398
|
plugins: parsePluginsConfig(parsed.plugins),
|
|
399
|
+
askUserPromptTimeoutSeconds: typeof parsed.askUserPromptTimeoutSeconds === "number"
|
|
400
|
+
? parsed.askUserPromptTimeoutSeconds
|
|
401
|
+
: defaults.askUserPromptTimeoutSeconds,
|
|
388
402
|
};
|
|
389
403
|
|
|
390
404
|
// Compute resolvedTrustedNetworks: merge trustedNetworks + auth.bypassHosts
|
|
@@ -38,4 +38,21 @@ export interface SessionDiffResponse {
|
|
|
38
38
|
files: FileDiffEntry[];
|
|
39
39
|
/** Whether the session cwd is a git repository */
|
|
40
40
|
isGitRepo: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* VCS regime used to compute the per-file diffs. Optional for
|
|
43
|
+
* backwards compatibility — absent on responses produced before
|
|
44
|
+
* change `add-jj-workspace-plugin`.
|
|
45
|
+
*/
|
|
46
|
+
vcsKind?: "git" | "jj";
|
|
47
|
+
/**
|
|
48
|
+
* The literal revset / ref used as the diff base (e.g. "HEAD",
|
|
49
|
+
* "@-", "fork_point(@, trunk())"). Optional.
|
|
50
|
+
*/
|
|
51
|
+
diffBase?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Human-readable label for `diffBase` (e.g. "develop", "trunk()",
|
|
54
|
+
* "HEAD"). Optional. Renders as "Diffing against \<baseLabel\>"
|
|
55
|
+
* in the client when `vcsKind === "jj"`.
|
|
56
|
+
*/
|
|
57
|
+
baseLabel?: string;
|
|
41
58
|
}
|