@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
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jujutsu (jj) tool module — Recipe-based API for jj operations the
|
|
3
|
+
* dashboard runs from multiple call sites (bridge VCS probe, jj-plugin
|
|
4
|
+
* server routes, session-diff regime-aware enrichment).
|
|
5
|
+
*
|
|
6
|
+
* Mirror of `platform/git.ts`: every function is a thin wrapper over
|
|
7
|
+
* `run(recipe, input)`. No `child_process` imports, no `process.platform`
|
|
8
|
+
* branches, no inline shell-escape logic.
|
|
9
|
+
*
|
|
10
|
+
* **Minimum jj version**: target `>= 0.18.0` for `workspace add -r`,
|
|
11
|
+
* `op restore`, `fork_point()`, and `--no-pager`. The version is
|
|
12
|
+
* captured in tool-registry metadata only; no runtime gate yet.
|
|
13
|
+
*
|
|
14
|
+
* **Output parsing strategy**: `jj` does not have a stable `--json` flag
|
|
15
|
+
* across the commands we use. Where parsing is required (`workspaceList`,
|
|
16
|
+
* `workspaceRoot`, `version`), we parse the human-readable output with
|
|
17
|
+
* defensive regexes. Mutation commands (`workspaceAdd`, `bookmarkCreate`,
|
|
18
|
+
* etc.) just check exit codes.
|
|
19
|
+
*
|
|
20
|
+
* See change: add-jj-workspace-plugin.
|
|
21
|
+
*/
|
|
22
|
+
import { run, unwrap, type Recipe, type Result } from "./runner.js";
|
|
23
|
+
|
|
24
|
+
// ── Recipes (pure data) ─────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const JJ_TIMEOUT = 15_000;
|
|
27
|
+
|
|
28
|
+
interface WithCwd {
|
|
29
|
+
cwd: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `jj --version` → semver string (e.g. "jj 0.18.0"). */
|
|
33
|
+
export const JJ_VERSION: Recipe<{}, string | undefined> = {
|
|
34
|
+
argv: () => ["jj", "--version"],
|
|
35
|
+
parse: (out) => {
|
|
36
|
+
const m = out.match(/jj\s+([0-9]+\.[0-9]+\.[0-9]+)/);
|
|
37
|
+
return m ? m[1] : out.trim() || undefined;
|
|
38
|
+
},
|
|
39
|
+
timeout: JJ_TIMEOUT,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `jj workspace root` → absolute path of the current workspace's root.
|
|
44
|
+
* Errors when cwd is not inside a jj repo.
|
|
45
|
+
*/
|
|
46
|
+
export const JJ_WORKSPACE_ROOT: Recipe<WithCwd, string | undefined> = {
|
|
47
|
+
argv: () => ["jj", "workspace", "root"],
|
|
48
|
+
parse: (out) => out.trim() || undefined,
|
|
49
|
+
timeout: JJ_TIMEOUT,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `jj workspace list` → raw output, one workspace per line.
|
|
54
|
+
* Format (jj 0.18+): `<name>: <change-id-short> <commit-id-short> (...) <desc>`
|
|
55
|
+
* Caller parses via `parseWorkspaceList`.
|
|
56
|
+
*/
|
|
57
|
+
export const JJ_WORKSPACE_LIST: Recipe<WithCwd, string> = {
|
|
58
|
+
argv: () => ["jj", "workspace", "list", "--no-pager"],
|
|
59
|
+
parse: (out) => out,
|
|
60
|
+
timeout: JJ_TIMEOUT,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `jj workspace add <abs-path> [-r <rev>]` — non-destructive on the
|
|
65
|
+
* source workspace; creates a new working-copy commit on top of `rev`.
|
|
66
|
+
*/
|
|
67
|
+
export const JJ_WORKSPACE_ADD: Recipe<
|
|
68
|
+
WithCwd & { destPath: string; baseRev?: string },
|
|
69
|
+
void
|
|
70
|
+
> = {
|
|
71
|
+
argv: ({ destPath, baseRev }) => {
|
|
72
|
+
const argv: string[] = ["jj", "workspace", "add", destPath];
|
|
73
|
+
if (baseRev) argv.push("-r", baseRev);
|
|
74
|
+
return argv;
|
|
75
|
+
},
|
|
76
|
+
parse: () => undefined,
|
|
77
|
+
timeout: JJ_TIMEOUT,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** `jj workspace forget <name>` — detaches without deleting files on disk. */
|
|
81
|
+
export const JJ_WORKSPACE_FORGET: Recipe<WithCwd & { name: string }, void> = {
|
|
82
|
+
argv: ({ name }) => ["jj", "workspace", "forget", name],
|
|
83
|
+
parse: () => undefined,
|
|
84
|
+
timeout: JJ_TIMEOUT,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** `jj bookmark create <name> -r <rev>`. */
|
|
88
|
+
export const JJ_BOOKMARK_CREATE: Recipe<
|
|
89
|
+
WithCwd & { name: string; rev: string },
|
|
90
|
+
void
|
|
91
|
+
> = {
|
|
92
|
+
argv: ({ name, rev }) => ["jj", "bookmark", "create", name, "-r", rev],
|
|
93
|
+
parse: () => undefined,
|
|
94
|
+
timeout: JJ_TIMEOUT,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `jj bookmark list -T 'name ++ "\n"'` — list bookmark names, one per line.
|
|
99
|
+
* Used by fold-back to check whether a bookmark name already exists.
|
|
100
|
+
*/
|
|
101
|
+
export const JJ_BOOKMARK_LIST: Recipe<WithCwd, string> = {
|
|
102
|
+
argv: () => ["jj", "bookmark", "list", "-T", 'name ++ "\\n"', "--no-pager"],
|
|
103
|
+
parse: (out) => out,
|
|
104
|
+
timeout: JJ_TIMEOUT,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** `jj git init --colocate` — converts a plain-git cwd into a jj-colocated repo. */
|
|
108
|
+
export const JJ_GIT_INIT_COLOCATE: Recipe<WithCwd, void> = {
|
|
109
|
+
argv: () => ["jj", "git", "init", "--colocate"],
|
|
110
|
+
parse: () => undefined,
|
|
111
|
+
timeout: JJ_TIMEOUT,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** `jj git push --bookmark <name>` — translates jj history to git refs. */
|
|
115
|
+
export const JJ_GIT_PUSH: Recipe<WithCwd & { bookmark: string }, void> = {
|
|
116
|
+
argv: ({ bookmark }) => ["jj", "git", "push", "--bookmark", bookmark],
|
|
117
|
+
parse: () => undefined,
|
|
118
|
+
timeout: JJ_TIMEOUT,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `jj diff [--from R1] [--to R2] [-- <path>]` — unified diff output.
|
|
123
|
+
* Default invocation diffs the working copy (`@`) against its parent (`@-`).
|
|
124
|
+
*/
|
|
125
|
+
export const JJ_DIFF: Recipe<
|
|
126
|
+
WithCwd & { fromRev?: string; toRev?: string; path?: string },
|
|
127
|
+
string
|
|
128
|
+
> = {
|
|
129
|
+
argv: ({ fromRev, toRev, path }) => {
|
|
130
|
+
const argv: string[] = ["jj", "diff", "--no-pager"];
|
|
131
|
+
if (fromRev) argv.push("--from", fromRev);
|
|
132
|
+
if (toRev) argv.push("--to", toRev);
|
|
133
|
+
if (path) argv.push("--", path);
|
|
134
|
+
return argv;
|
|
135
|
+
},
|
|
136
|
+
parse: (out) => out,
|
|
137
|
+
timeout: JJ_TIMEOUT,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `jj resolve --list` — newline-separated list of files with conflicts.
|
|
142
|
+
* Empty output means no conflicts; tolerated exit 1 for "nothing to resolve".
|
|
143
|
+
*/
|
|
144
|
+
export const JJ_RESOLVE_LIST: Recipe<WithCwd, string> = {
|
|
145
|
+
argv: () => ["jj", "resolve", "--list", "--no-pager"],
|
|
146
|
+
parse: (out) => out,
|
|
147
|
+
timeout: JJ_TIMEOUT,
|
|
148
|
+
tolerate: [1],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* `jj op log -T 'id.short() ++ "\n"' --limit 1` — current op id (short).
|
|
153
|
+
* Used by fold-back to capture pre-rebase state for `op restore`.
|
|
154
|
+
*/
|
|
155
|
+
export const JJ_OP_LOG_HEAD: Recipe<WithCwd, string | undefined> = {
|
|
156
|
+
argv: () => [
|
|
157
|
+
"jj", "op", "log",
|
|
158
|
+
"-T", 'id.short() ++ "\\n"',
|
|
159
|
+
"--limit", "1",
|
|
160
|
+
"--no-pager",
|
|
161
|
+
],
|
|
162
|
+
parse: (out) => out.trim().split("\n")[0]?.trim() || undefined,
|
|
163
|
+
timeout: JJ_TIMEOUT,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** `jj op restore <op-id>` — undo back to the given operation. */
|
|
167
|
+
export const JJ_OP_RESTORE: Recipe<WithCwd & { opId: string }, void> = {
|
|
168
|
+
argv: ({ opId }) => ["jj", "op", "restore", opId],
|
|
169
|
+
parse: () => undefined,
|
|
170
|
+
timeout: JJ_TIMEOUT,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** `jj rebase -d <dest> -s <src>` — rebase src and descendants onto dest. */
|
|
174
|
+
export const JJ_REBASE: Recipe<
|
|
175
|
+
WithCwd & { dest: string; src: string },
|
|
176
|
+
void
|
|
177
|
+
> = {
|
|
178
|
+
argv: ({ dest, src }) => ["jj", "rebase", "-d", dest, "-s", src],
|
|
179
|
+
parse: () => undefined,
|
|
180
|
+
timeout: JJ_TIMEOUT,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `jj log -r '<revset>' -T 'change_id.short() ++ "\n"'` —
|
|
185
|
+
* list change ids matching a revset, one per line.
|
|
186
|
+
* Used to check for unfolded commits and resolve `fork_point()`.
|
|
187
|
+
*/
|
|
188
|
+
export const JJ_LOG_REVSET: Recipe<
|
|
189
|
+
WithCwd & { revset: string; template?: string },
|
|
190
|
+
string
|
|
191
|
+
> = {
|
|
192
|
+
argv: ({ revset, template }) => [
|
|
193
|
+
"jj", "log",
|
|
194
|
+
"-r", revset,
|
|
195
|
+
"-T", template ?? 'change_id.short() ++ "\\n"',
|
|
196
|
+
"--no-pager",
|
|
197
|
+
"--no-graph",
|
|
198
|
+
],
|
|
199
|
+
parse: (out) => out,
|
|
200
|
+
timeout: JJ_TIMEOUT,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ── Registry ────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
export const JJ_RECIPES = {
|
|
206
|
+
JJ_VERSION,
|
|
207
|
+
JJ_WORKSPACE_ROOT,
|
|
208
|
+
JJ_WORKSPACE_LIST,
|
|
209
|
+
JJ_WORKSPACE_ADD,
|
|
210
|
+
JJ_WORKSPACE_FORGET,
|
|
211
|
+
JJ_BOOKMARK_CREATE,
|
|
212
|
+
JJ_BOOKMARK_LIST,
|
|
213
|
+
JJ_GIT_INIT_COLOCATE,
|
|
214
|
+
JJ_GIT_PUSH,
|
|
215
|
+
JJ_DIFF,
|
|
216
|
+
JJ_RESOLVE_LIST,
|
|
217
|
+
JJ_OP_LOG_HEAD,
|
|
218
|
+
JJ_OP_RESTORE,
|
|
219
|
+
JJ_REBASE,
|
|
220
|
+
JJ_LOG_REVSET,
|
|
221
|
+
} as const;
|
|
222
|
+
|
|
223
|
+
// ── Public typed API ────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
export function version(): Result<string | undefined> {
|
|
226
|
+
return run(JJ_VERSION, {}, {});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function workspaceRoot(input: WithCwd): Result<string | undefined> {
|
|
230
|
+
return run(JJ_WORKSPACE_ROOT, input, { cwd: input.cwd });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function workspaceList(input: WithCwd): Result<string> {
|
|
234
|
+
return run(JJ_WORKSPACE_LIST, input, { cwd: input.cwd });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function workspaceAdd(
|
|
238
|
+
input: WithCwd & { destPath: string; baseRev?: string },
|
|
239
|
+
): Result<void> {
|
|
240
|
+
return run(JJ_WORKSPACE_ADD, input, { cwd: input.cwd });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function workspaceForget(
|
|
244
|
+
input: WithCwd & { name: string },
|
|
245
|
+
): Result<void> {
|
|
246
|
+
return run(JJ_WORKSPACE_FORGET, input, { cwd: input.cwd });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function bookmarkCreate(
|
|
250
|
+
input: WithCwd & { name: string; rev: string },
|
|
251
|
+
): Result<void> {
|
|
252
|
+
return run(JJ_BOOKMARK_CREATE, input, { cwd: input.cwd });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function bookmarkList(input: WithCwd): Result<string> {
|
|
256
|
+
return run(JJ_BOOKMARK_LIST, input, { cwd: input.cwd });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function gitInitColocate(input: WithCwd): Result<void> {
|
|
260
|
+
return run(JJ_GIT_INIT_COLOCATE, input, { cwd: input.cwd });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function gitPush(
|
|
264
|
+
input: WithCwd & { bookmark: string },
|
|
265
|
+
): Result<void> {
|
|
266
|
+
return run(JJ_GIT_PUSH, input, { cwd: input.cwd });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function diff(
|
|
270
|
+
input: WithCwd & { fromRev?: string; toRev?: string; path?: string },
|
|
271
|
+
): Result<string> {
|
|
272
|
+
return run(JJ_DIFF, input, { cwd: input.cwd });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function resolveList(input: WithCwd): Result<string> {
|
|
276
|
+
return run(JJ_RESOLVE_LIST, input, { cwd: input.cwd });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function opLogHead(input: WithCwd): Result<string | undefined> {
|
|
280
|
+
return run(JJ_OP_LOG_HEAD, input, { cwd: input.cwd });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function opRestore(
|
|
284
|
+
input: WithCwd & { opId: string },
|
|
285
|
+
): Result<void> {
|
|
286
|
+
return run(JJ_OP_RESTORE, input, { cwd: input.cwd });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function rebase(
|
|
290
|
+
input: WithCwd & { dest: string; src: string },
|
|
291
|
+
): Result<void> {
|
|
292
|
+
return run(JJ_REBASE, input, { cwd: input.cwd });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function logRevset(
|
|
296
|
+
input: WithCwd & { revset: string; template?: string },
|
|
297
|
+
): Result<string> {
|
|
298
|
+
return run(JJ_LOG_REVSET, input, { cwd: input.cwd });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Best-effort wrappers ────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
export function versionOr(fallback?: string): string | undefined {
|
|
304
|
+
return unwrap(version(), fallback);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function workspaceRootOr(
|
|
308
|
+
input: WithCwd,
|
|
309
|
+
fallback?: string,
|
|
310
|
+
): string | undefined {
|
|
311
|
+
return unwrap(workspaceRoot(input), fallback);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function workspaceListOr(input: WithCwd, fallback = ""): string {
|
|
315
|
+
return unwrap(workspaceList(input), fallback);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function diffOr(
|
|
319
|
+
input: WithCwd & { fromRev?: string; toRev?: string; path?: string },
|
|
320
|
+
fallback = "",
|
|
321
|
+
): string {
|
|
322
|
+
return unwrap(diff(input), fallback);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function resolveListOr(input: WithCwd, fallback = ""): string {
|
|
326
|
+
return unwrap(resolveList(input), fallback);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function opLogHeadOr(
|
|
330
|
+
input: WithCwd,
|
|
331
|
+
fallback?: string,
|
|
332
|
+
): string | undefined {
|
|
333
|
+
return unwrap(opLogHead(input), fallback);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function bookmarkListOr(input: WithCwd, fallback = ""): string {
|
|
337
|
+
return unwrap(bookmarkList(input), fallback);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Pure parsers (separate from I/O for unit testability) ───────────────────
|
|
341
|
+
|
|
342
|
+
export interface JjWorkspaceEntry {
|
|
343
|
+
/** Workspace name (e.g. "default", "agent-1"). */
|
|
344
|
+
name: string;
|
|
345
|
+
/** Short change id of the workspace's working-copy commit. */
|
|
346
|
+
changeIdShort?: string;
|
|
347
|
+
/** Short commit id (the underlying git commit when colocated). */
|
|
348
|
+
commitIdShort?: string;
|
|
349
|
+
/** Working-copy description, if any. */
|
|
350
|
+
description?: string;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Parse `jj workspace list` output into structured entries.
|
|
355
|
+
* Format: `<name>: <change-id-short> <commit-id-short> [(empty)] [(no description set) | <desc>]`
|
|
356
|
+
*
|
|
357
|
+
* Defensive: skips lines that don't match the expected shape.
|
|
358
|
+
*/
|
|
359
|
+
export function parseWorkspaceList(raw: string): JjWorkspaceEntry[] {
|
|
360
|
+
const entries: JjWorkspaceEntry[] = [];
|
|
361
|
+
for (const line of raw.split("\n")) {
|
|
362
|
+
const trimmed = line.trim();
|
|
363
|
+
if (!trimmed) continue;
|
|
364
|
+
const colonIdx = trimmed.indexOf(":");
|
|
365
|
+
if (colonIdx <= 0) continue;
|
|
366
|
+
const name = trimmed.slice(0, colonIdx).trim();
|
|
367
|
+
const rest = trimmed.slice(colonIdx + 1).trim();
|
|
368
|
+
if (!name) continue;
|
|
369
|
+
// The remainder typically starts with two short ids separated by space.
|
|
370
|
+
const idMatch = rest.match(/^([0-9a-z]+)\s+([0-9a-f]+)/i);
|
|
371
|
+
const entry: JjWorkspaceEntry = { name };
|
|
372
|
+
if (idMatch) {
|
|
373
|
+
entry.changeIdShort = idMatch[1];
|
|
374
|
+
entry.commitIdShort = idMatch[2];
|
|
375
|
+
// Strip jj's parenthesized markers ((empty), (no description set),
|
|
376
|
+
// (conflict), etc.) and only keep what's left as the description.
|
|
377
|
+
let after = rest.slice(idMatch[0].length).trim();
|
|
378
|
+
while (/^\([^)]*\)/.test(after)) {
|
|
379
|
+
after = after.replace(/^\([^)]*\)\s*/, "");
|
|
380
|
+
}
|
|
381
|
+
if (after) {
|
|
382
|
+
entry.description = after;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
entries.push(entry);
|
|
386
|
+
}
|
|
387
|
+
return entries;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Given a workspace root absolute path and the parsed workspace list, find
|
|
392
|
+
* the workspace name whose working copy lives at that path. Returns
|
|
393
|
+
* `undefined` if no entry matches.
|
|
394
|
+
*
|
|
395
|
+
* Note: `jj workspace list` does not include the workspace path; resolution
|
|
396
|
+
* by name happens via the bridge probe checking `<workspace-name>@` revsets
|
|
397
|
+
* separately. For the bridge's purposes, we ALSO read `.jj/repo/working_copy/`
|
|
398
|
+
* filesystem layout — this parser is a structural fallback only.
|
|
399
|
+
*/
|
|
400
|
+
export function findWorkspaceByName(
|
|
401
|
+
entries: readonly JjWorkspaceEntry[],
|
|
402
|
+
name: string,
|
|
403
|
+
): JjWorkspaceEntry | undefined {
|
|
404
|
+
return entries.find((e) => e.name === name);
|
|
405
|
+
}
|
|
@@ -130,6 +130,19 @@ export interface GitInfoUpdateMessage {
|
|
|
130
130
|
gitPrUrl?: string;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Bridge → server: jj workspace state for the session's cwd.
|
|
135
|
+
* Sent only when the bridge's VCS probe finds `.jj/` AND `jj` resolves
|
|
136
|
+
* via the tool registry. Cleared (sent with `jjState: null`) when the
|
|
137
|
+
* session leaves a jj repo (e.g. cwd switch). See change: add-jj-workspace-plugin.
|
|
138
|
+
*/
|
|
139
|
+
export interface JjStateUpdateMessage {
|
|
140
|
+
type: "jj_state_update";
|
|
141
|
+
sessionId: string;
|
|
142
|
+
/** `null` clears the session's `jjState` field on the server. */
|
|
143
|
+
jjState: import("./types.js").JjState | null;
|
|
144
|
+
}
|
|
145
|
+
|
|
133
146
|
// OpenSpecUpdateMessage removed — server polls directly via DirectoryService
|
|
134
147
|
|
|
135
148
|
export interface ModelsListMessage {
|
|
@@ -283,6 +296,7 @@ export type ExtensionToServerMessage =
|
|
|
283
296
|
| ExtensionUiRequestMessage
|
|
284
297
|
| FilesListMessage
|
|
285
298
|
| GitInfoUpdateMessage
|
|
299
|
+
| JjStateUpdateMessage
|
|
286
300
|
| SessionNameUpdateMessage
|
|
287
301
|
| ModelsListMessage
|
|
288
302
|
| ModelUpdateMessage
|
|
@@ -354,6 +354,7 @@ export function registerDefaultTools(registry: ToolRegistry, deps?: StrategyDeps
|
|
|
354
354
|
// Native binaries — no interpreter needed.
|
|
355
355
|
registry.register(binaryDef("node", deps));
|
|
356
356
|
registry.register(binaryDef("git", deps));
|
|
357
|
+
registry.register(binaryDef("jj", deps));
|
|
357
358
|
registry.register(binaryDef("zrok", deps));
|
|
358
359
|
|
|
359
360
|
// Platform-conditional process-inspection utilities. These are only
|
|
@@ -4,6 +4,31 @@ export type SessionSource = "tui" | "zed" | "tmux" | "dashboard" | "terminal" |
|
|
|
4
4
|
/** Current status of a session */
|
|
5
5
|
export type SessionStatus = "active" | "idle" | "streaming" | "ended";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Per-session jj (Jujutsu) probe state. Populated by the bridge when the
|
|
9
|
+
* cwd contains a `.jj/` directory and the `jj` tool resolves; otherwise
|
|
10
|
+
* left undefined.
|
|
11
|
+
* See change: add-jj-workspace-plugin.
|
|
12
|
+
*/
|
|
13
|
+
export interface JjState {
|
|
14
|
+
/** True iff cwd is inside a jj repo (`.jj/` reachable). */
|
|
15
|
+
isJjRepo: boolean;
|
|
16
|
+
/** True iff the repo is jj-colocated with git (both `.jj/` and `.git/`). */
|
|
17
|
+
isColocated: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Name of the workspace whose working copy is at this cwd.
|
|
20
|
+
* `"default"` for the original/source workspace; user-named for siblings
|
|
21
|
+
* created via `jj workspace add`.
|
|
22
|
+
*/
|
|
23
|
+
workspaceName?: string;
|
|
24
|
+
/** Absolute path of the workspace root (== cwd for the active workspace). */
|
|
25
|
+
workspaceRoot?: string;
|
|
26
|
+
/** Bookmarks present on the workspace's `@-` (used by the badge / fold-back). */
|
|
27
|
+
bookmarks?: string[];
|
|
28
|
+
/** Last probe error, surfaced for diagnostics. Empty when probe succeeded. */
|
|
29
|
+
lastError?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
7
32
|
/** A dashboard session representing a connected pi instance */
|
|
8
33
|
export interface DashboardSession {
|
|
9
34
|
id: string;
|
|
@@ -44,6 +69,15 @@ export interface DashboardSession {
|
|
|
44
69
|
gitBranchUrl?: string;
|
|
45
70
|
gitPrNumber?: number;
|
|
46
71
|
gitPrUrl?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Per-session jj (Jujutsu) state. Populated by the bridge's per-session
|
|
74
|
+
* VCS probe when (a) the tool registry resolves `jj` AND (b) `.jj/` exists
|
|
75
|
+
* in the session cwd. Absent or `{ isJjRepo: false }` for sessions outside
|
|
76
|
+
* a jj repo — jj-plugin slot predicates treat both as inactive. NOT
|
|
77
|
+
* persisted to `.meta.json`; refreshed on every probe tick.
|
|
78
|
+
* See change: add-jj-workspace-plugin.
|
|
79
|
+
*/
|
|
80
|
+
jjState?: JjState;
|
|
47
81
|
openspecPhase?: OpenSpecPhase | null;
|
|
48
82
|
openspecChange?: string | null;
|
|
49
83
|
attachedProposal?: string | null;
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git info gathering — detects branch, remote URL, and PR number.
|
|
3
|
-
* Delegates to the shared git tool module so there's no inline execSync
|
|
4
|
-
* and every call benefits from the runner's safety defaults (windowsHide,
|
|
5
|
-
* timeout, tolerated exit codes).
|
|
6
|
-
* See change: platform-command-executor.
|
|
7
|
-
*/
|
|
8
|
-
import * as git from "@blackbelt-technology/pi-dashboard-shared/platform/git.js";
|
|
9
|
-
import { buildGitLinks, type GitLinks } from "./git-link-builder.js";
|
|
10
|
-
|
|
11
|
-
export interface GitInfo {
|
|
12
|
-
gitBranch: string;
|
|
13
|
-
gitBranchUrl?: string;
|
|
14
|
-
gitPrNumber?: number;
|
|
15
|
-
gitPrUrl?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Detect the current git branch. Returns short SHA for detached HEAD. */
|
|
19
|
-
export function detectBranch(cwd: string): string | undefined {
|
|
20
|
-
const ref = git.currentBranchOr({ cwd });
|
|
21
|
-
if (!ref) return undefined;
|
|
22
|
-
if (ref === "HEAD") {
|
|
23
|
-
// Detached HEAD — return short commit SHA
|
|
24
|
-
return git.headShaOr({ cwd, short: true }) ?? "HEAD";
|
|
25
|
-
}
|
|
26
|
-
return ref;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Detect the remote origin URL. */
|
|
30
|
-
export function detectRemoteUrl(cwd: string): string | undefined {
|
|
31
|
-
return git.remoteUrlOr({ cwd });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Detect the PR number via gh CLI (best effort). */
|
|
35
|
-
export function detectPrNumber(cwd: string): number | undefined {
|
|
36
|
-
return git.prNumberOr({ cwd });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Gather all git info for a directory. Returns undefined if not a git repo. */
|
|
40
|
-
export function gatherGitInfo(cwd: string): GitInfo | undefined {
|
|
41
|
-
const branch = detectBranch(cwd);
|
|
42
|
-
if (!branch) return undefined;
|
|
43
|
-
|
|
44
|
-
const remoteUrl = detectRemoteUrl(cwd);
|
|
45
|
-
const prNumber = detectPrNumber(cwd);
|
|
46
|
-
|
|
47
|
-
const links: GitLinks = remoteUrl ? buildGitLinks(remoteUrl, branch, prNumber) : {};
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
gitBranch: branch,
|
|
51
|
-
gitBranchUrl: links.branchUrl,
|
|
52
|
-
gitPrNumber: prNumber,
|
|
53
|
-
gitPrUrl: links.prUrl,
|
|
54
|
-
};
|
|
55
|
-
}
|