@timurproko/a1 0.2.7-dev.1287 → 0.2.7-dev.1290

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.
Files changed (50) hide show
  1. package/README.md +4 -0
  2. package/bin/cli.js +4 -0
  3. package/dist/cli/capabilities.d.ts +1 -0
  4. package/dist/cli/capabilities.js +1 -1
  5. package/dist/cli/dispatch.d.ts +4 -0
  6. package/dist/cli/dispatch.js +34 -1
  7. package/dist/cli/session-color.d.ts +6 -0
  8. package/dist/cli/session-color.js +44 -0
  9. package/dist/cli/session-context.js +5 -0
  10. package/dist/composition/owned-settings.d.ts +2 -2
  11. package/dist/composition/owned-settings.js +2 -2
  12. package/dist/composition/owned-ui.js +31 -11
  13. package/dist/composition/session-colors.d.ts +5 -0
  14. package/dist/composition/session-colors.js +33 -0
  15. package/dist/composition/session-runtime-registry.d.ts +3 -1
  16. package/dist/composition/session-runtime-registry.js +32 -3
  17. package/dist/contracts/owned-ui/index.d.ts +2 -2
  18. package/dist/contracts/owned-ui/index.js +1 -0
  19. package/dist/contracts/presentation/progress-palette.generated.d.ts +31 -5
  20. package/dist/contracts/presentation/progress-palette.generated.js +1 -1
  21. package/dist/features/launch/session-context.d.ts +9 -0
  22. package/dist/features/launch/session-context.js +7 -1
  23. package/dist/features/owned-ui/settings-app.js +1 -1
  24. package/dist/foundation/lifecycle/index.d.ts +6 -2
  25. package/dist/foundation/lifecycle/index.js +3 -1
  26. package/dist/foundation/lifecycle/session-control.d.ts +26 -0
  27. package/dist/foundation/lifecycle/session-control.js +222 -0
  28. package/dist/foundation/lifecycle/session-repository-context.d.ts +27 -0
  29. package/dist/foundation/lifecycle/session-repository-context.js +106 -7
  30. package/dist/foundation/lifecycle/session-worktree-completion.d.ts +35 -0
  31. package/dist/foundation/lifecycle/session-worktree-completion.js +190 -0
  32. package/dist/foundation/release/early-accent.d.ts +2 -1
  33. package/dist/integrations/pi/components/upstream/theme/theme.js +8 -6
  34. package/dist/integrations/pi/engine/runtime-integration.js +1 -1
  35. package/dist/integrations/pi/engine/worktree-tools.d.ts +5 -2
  36. package/dist/integrations/pi/engine/worktree-tools.js +18 -3
  37. package/dist/ui/settings/declarations.d.ts +18 -2
  38. package/dist/ui/settings/declarations.js +14 -4
  39. package/dist/ui/settings/early-accent.d.ts +16 -0
  40. package/dist/ui/settings/early-accent.js +1 -3
  41. package/dist/ui/settings/manager.d.ts +4 -2
  42. package/dist/ui/settings/manager.js +36 -4
  43. package/dist/ui/settings/resolution.js +3 -2
  44. package/docs/architecture/project-structure.md +1 -1
  45. package/docs/features/session-colors.md +66 -0
  46. package/docs/features/worktrees.md +30 -2
  47. package/docs/local-worktree-cleanup.md +3 -1
  48. package/docs/openspec-archive-automation.md +3 -3
  49. package/docs/worktree-link-recovery.md +2 -0
  50. package/package.json +3 -3
package/README.md CHANGED
@@ -99,6 +99,10 @@ a1 pi
99
99
 
100
100
  <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/assets/readme/separator-dark.svg"><img src="docs/assets/readme/separator.svg" alt="" width="280"></picture></p>
101
101
 
102
+ ## Session colors
103
+
104
+ Settings → Appearance separates a saved **Default color** (initially blue) from **Current color**, which affects only this session. Both offer blue, green, orange, pink, purple, cyan, and grey. External applications can read and change a targeted session through the [always-JSON local color API](docs/features/session-colors.md).
105
+
102
106
  ## Update
103
107
 
104
108
  Update on the channel you already use (release stays on release, develop stays on develop):
package/bin/cli.js CHANGED
@@ -54,6 +54,10 @@ process.exitCode = await dispatchCli(process.argv.slice(2), {
54
54
  const { createPiPackagesPort } = await import("../dist/integrations/pi/engine/package-integration.js");
55
55
  return await runPackageCommand(request, { createPort: createPiPackagesPort });
56
56
  },
57
+ sessionColor: async arguments_ => {
58
+ const { runSessionColorCommand } = await import("../dist/cli/session-color.js");
59
+ return await runSessionColorCommand(arguments_);
60
+ },
57
61
  sessionContext: async request => {
58
62
  const { runSessionContextCommand } = await import("../dist/cli/session-context.js");
59
63
  return await runSessionContextCommand(request);
@@ -1,6 +1,7 @@
1
1
  /** The Pi comparison launch is a prerelease development instrument, not product. */
2
2
  export interface CliCapabilities {
3
3
  readonly developmentComparison: boolean;
4
+ readonly sessionColors?: boolean;
4
5
  /** Bare `update` stays on this build's channel; absent means stable. */
5
6
  readonly runningChannel?: "stable" | "next";
6
7
  }
@@ -1,7 +1,7 @@
1
1
  import { prerelease } from "semver";
2
2
  export function cliCapabilities(version) {
3
3
  const development = isPrereleaseVersion(version);
4
- return Object.freeze({ developmentComparison: development, runningChannel: development ? "next" : "stable" });
4
+ return Object.freeze({ developmentComparison: development, sessionColors: true, runningChannel: development ? "next" : "stable" });
5
5
  }
6
6
  export function isPrereleaseVersion(version) {
7
7
  return (prerelease(version)?.length ?? 0) > 0;
@@ -10,6 +10,7 @@ export interface CliHandlers {
10
10
  readonly update: (channel: UpdateChannel, target?: string) => Promise<number>;
11
11
  readonly packages: (request: PackageCommandRequest) => Promise<number>;
12
12
  readonly sessionContext: (request: SessionContextRequest) => Promise<number>;
13
+ readonly sessionColor?: (arguments_: readonly string[]) => Promise<number>;
13
14
  /** Removes A1 from the machine; absent in hosts that cannot uninstall. */
14
15
  readonly uninstall?: (request: {
15
16
  readonly yes: boolean;
@@ -51,6 +52,9 @@ export type CliCommand = {
51
52
  } | {
52
53
  readonly kind: "session-context";
53
54
  readonly request: SessionContextRequest;
55
+ } | {
56
+ readonly kind: "session-color";
57
+ readonly arguments: readonly string[];
54
58
  } | {
55
59
  readonly kind: "uninstall";
56
60
  readonly request: {
@@ -23,7 +23,9 @@ export function cliUsage(capabilities) {
23
23
  "worktree enter <branch> [--base <branch>]",
24
24
  "worktree attach <path>",
25
25
  "worktree detach",
26
+ "worktree complete [--change <id>] [--pr <number>]",
26
27
  "worktree list",
28
+ "session color list | get [--session <id>] | set <color> [--session <id>]",
27
29
  "session worktrees",
28
30
  "session link-worktree <path>",
29
31
  "session unlink-worktree",
@@ -59,8 +61,14 @@ export function cliHelp(capabilities) {
59
61
  ` ${command} worktree enter <branch> [--base <branch>]`,
60
62
  ` ${command} worktree attach <path>`,
61
63
  ` ${command} worktree detach`,
64
+ ` ${command} worktree complete [--change <id>] [--pr <number>]`,
62
65
  ` ${command} worktree list`,
63
66
  "",
67
+ "Session colors (always JSON; explicit session id required outside the session shell):",
68
+ ` ${command} session color list`,
69
+ ` ${command} session color get --session <id>`,
70
+ ` ${command} session color set <color> --session <id>`,
71
+ "",
64
72
  "Session context (compatibility aliases):",
65
73
  ` ${command} session worktrees`,
66
74
  ` ${command} session link-worktree <path>`,
@@ -97,6 +105,12 @@ export async function dispatchCli(arguments_, handlers, output, capabilities) {
97
105
  return await handlers.packages(command.request);
98
106
  if (command.kind === "session-context")
99
107
  return await handlers.sessionContext(command.request);
108
+ if (command.kind === "session-color") {
109
+ if (handlers.sessionColor)
110
+ return await handlers.sessionColor(command.arguments);
111
+ output.stdout(`${JSON.stringify({ error: { code: "control-unavailable", message: "Session color API is unavailable in this host." } })}\n`);
112
+ return 1;
113
+ }
100
114
  if (command.kind === "uninstall")
101
115
  return handlers.uninstall ? await handlers.uninstall(command.request) : 2;
102
116
  return await handlers.update(command.channel, command.target);
@@ -135,6 +149,8 @@ export function parseCliCommand(arguments_, capabilities) {
135
149
  return parseUpdate(rest, capabilities.runningChannel ?? "stable");
136
150
  if (command === "worktree")
137
151
  return parseWorktree(rest);
152
+ if (command === "session" && rest[0] === "color")
153
+ return { kind: "session-color", arguments: rest.slice(1) };
138
154
  if (command === "session")
139
155
  return parseSessionContext(rest);
140
156
  if (command?.startsWith("update:"))
@@ -155,7 +171,24 @@ function parseWorktree(rest) {
155
171
  if ((action === "detach" || action === "list") && value === undefined) {
156
172
  return { kind: "session-context", request: { action: action === "detach" ? "unlink-worktree" : "worktrees" } };
157
173
  }
158
- return { kind: "error", message: PRODUCT_TEXT.diagnostic("worktree accepts enter <branch> [--base <branch>], attach <path>, detach, or list.") };
174
+ if (action === "complete") {
175
+ const selectors = rest.slice(1);
176
+ let change, pr;
177
+ let valid = selectors.length % 2 === 0;
178
+ for (let index = 0; valid && index < selectors.length; index += 2) {
179
+ const name = selectors[index], selected = selectors[index + 1] ?? "";
180
+ if (name === "--change" && change === undefined && /^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(selected))
181
+ change = selected;
182
+ else if (name === "--pr" && pr === undefined && /^[1-9][0-9]{0,8}$/u.test(selected))
183
+ pr = Number(selected);
184
+ else
185
+ valid = false;
186
+ }
187
+ if (valid) {
188
+ return { kind: "session-context", request: { action: "complete-worktree", ...(change === undefined ? {} : { change }), ...(pr === undefined ? {} : { pr }) } };
189
+ }
190
+ }
191
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic("worktree accepts enter <branch> [--base <branch>], attach <path>, detach, complete [--change <id>] [--pr <number>], or list.") };
159
192
  }
160
193
  function parseSessionContext(rest) {
161
194
  const [action, value, extra] = rest;
@@ -0,0 +1,6 @@
1
+ interface ColorCommandOptions {
2
+ readonly environment?: NodeJS.ProcessEnv;
3
+ readonly stdout?: (message: string) => void;
4
+ }
5
+ export declare function runSessionColorCommand(arguments_: readonly string[], options?: ColorCommandOptions): Promise<number>;
6
+ export {};
@@ -0,0 +1,44 @@
1
+ import { UI_ACCENT_COLORS } from "../contracts/owned-ui/index.js";
2
+ import { requestSessionControl, resolveProductPaths } from "../foundation/lifecycle/index.js";
3
+ import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../product-identity.js";
4
+ const failure = (code, message) => Object.assign(new Error(message), { code });
5
+ export async function runSessionColorCommand(arguments_, options = {}) {
6
+ const stdout = options.stdout ?? (message => process.stdout.write(message));
7
+ const environment = options.environment ?? process.env;
8
+ try {
9
+ const [action, ...rest] = arguments_;
10
+ if (action === "help" || action === "--help" || action === "-h") {
11
+ stdout(`${PRODUCT_TEXT.commandName} session color list | get [--session <id>] | set <color> [--session <id>]\nAlways JSON; get/set target Current color only. Outside an A1 shell, --session is required.\n`);
12
+ return 0;
13
+ }
14
+ if (action === "list" && rest.length === 0) {
15
+ stdout(`${JSON.stringify({ colors: UI_ACCENT_COLORS })}\n`);
16
+ return 0;
17
+ }
18
+ if (action !== "get" && action !== "set")
19
+ throw failure("invalid-request", "Use session color list, get, or set <color>.");
20
+ const color = action === "set" ? rest.shift() : undefined;
21
+ if (action === "set" && !UI_ACCENT_COLORS.includes(color))
22
+ throw failure("invalid-color", "Choose blue, green, orange, pink, purple, cyan, or grey.");
23
+ if (rest.length !== 0 && (rest.length !== 2 || rest[0] !== "--session" || !rest[1]))
24
+ throw failure("invalid-request", "Expected only --session followed by a complete session id.");
25
+ const explicit = rest.length === 2;
26
+ const runtimeId = environment[PRODUCT_IDENTITY.environment.sessionRuntimeId];
27
+ const sessionId = explicit ? rest[1] : runtimeId && environment.PI_SESSION_FILE ? environment.PI_SESSION_ID : undefined;
28
+ if (!sessionId)
29
+ throw failure("session-required", "Supply --session <id> outside an exact A1 shell runtime.");
30
+ const result = await requestSessionControl({
31
+ runtimeDir: resolveProductPaths(environment).runtimeDir, sessionId,
32
+ ...(!explicit && runtimeId ? { runtimeId } : {}),
33
+ payload: { action, ...(color === undefined ? {} : { color }) },
34
+ });
35
+ stdout(`${JSON.stringify(result)}\n`);
36
+ return 0;
37
+ }
38
+ catch (error) {
39
+ const detail = error;
40
+ const code = detail.code ?? "control-failed";
41
+ stdout(`${JSON.stringify({ error: { code, message: detail.message } })}\n`);
42
+ return ["invalid-request", "invalid-color", "invalid-session", "session-required"].includes(code) ? 2 : 1;
43
+ }
44
+ }
@@ -15,6 +15,9 @@ export async function runSessionContextCommand(request, options = {}) {
15
15
  const outcome = await updateSessionRepositoryContext({ sessionId: identity.sessionId, sessionFile: identity.sessionFile }, request, { runtimeId: identity.runtimeId, inspectProcess: options.inspectProcess ?? inspectLiveProcess });
16
16
  if (outcome.kind === "unlinked")
17
17
  stdout("Session worktree context cleared.\n");
18
+ if (outcome.kind === "completing") {
19
+ stdout(`Session returned to ${outcome.startupRoot}; left ${outcome.cwd} (${outcome.branch}). Repository cleanup runs when this command's shell exits and is reported with its result.\n`);
20
+ }
18
21
  if (outcome.kind === "linked")
19
22
  stdout(`Session worktree context linked: ${outcome.cwd} (${outcome.branch})\n`);
20
23
  if (outcome.kind === "worktrees") {
@@ -57,6 +60,8 @@ function recoveryDetail(code) {
57
60
  "worktree-primary-checkout": "attach a task worktree, not the primary checkout; use worktree enter to create one",
58
61
  "worktree-git-locked": "Git has locked this worktree; retain it and ask its owner to release the lock",
59
62
  "worktree-foreign-repository": "the selected worktree belongs to a different repository; the previous attachment was preserved",
63
+ "worktree-not-attached": "no task worktree is attached to this session; nothing was changed",
64
+ "worktree-attachment-invalid": "the attached worktree is no longer a valid Git worktree; run worktree detach to return to the startup checkout",
60
65
  "worktree-context-changed": "the attachment changed during the operation; no ownership was overridden",
61
66
  };
62
67
  return code === null ? undefined : details[code];
@@ -1,12 +1,12 @@
1
1
  import type { AgentSettingsPort } from "../contracts/agent-engine/index.js";
2
- import { OwnedSettingsManager } from "../ui/settings/manager.js";
2
+ import type { OwnedSettingsManager } from "../ui/settings/manager.js";
3
3
  interface OwnedSettingsManagerFactoryOptions {
4
4
  readonly profileId: string;
5
5
  readonly agent?: AgentSettingsPort | null;
6
6
  readonly agentProvider?: () => AgentSettingsPort | null;
7
7
  }
8
8
  /** Builds the profile-global settings manager shared by the Agent and slim utility compositions. */
9
- export declare function createOwnedSettingsManager(options: OwnedSettingsManagerFactoryOptions): OwnedSettingsManager;
9
+ export declare function createOwnedSettingsManager(options: OwnedSettingsManagerFactoryOptions): Promise<OwnedSettingsManager>;
10
10
  /** Rereads the shared settings document whenever its stat changes; returns the stop. */
11
11
  export declare function watchSharedSettings(settings: Pick<OwnedSettingsManager, "file" | "refresh">): () => void;
12
12
  export {};
@@ -1,9 +1,9 @@
1
1
  import { statSync } from "node:fs";
2
2
  import { resolveLaunchProfilePaths } from "../features/launch/profile-paths.js";
3
3
  import { PRODUCT_IDENTITY } from "../product-identity.js";
4
- import { OwnedSettingsManager } from "../ui/settings/manager.js";
5
4
  /** Builds the profile-global settings manager shared by the Agent and slim utility compositions. */
6
- export function createOwnedSettingsManager(options) {
5
+ export async function createOwnedSettingsManager(options) {
6
+ const { OwnedSettingsManager } = await import("../ui/settings/manager.js");
7
7
  const profilePaths = resolveLaunchProfilePaths();
8
8
  return new OwnedSettingsManager({
9
9
  profileDir: profilePaths.agentProfile,
@@ -39,7 +39,7 @@ export async function composeOwnedUi(options = {}) {
39
39
  let backend;
40
40
  const settings = options.profileId === undefined
41
41
  ? null
42
- : createOwnedSettingsManager({
42
+ : await createOwnedSettingsManager({
43
43
  profileId: options.profileId,
44
44
  agentProvider: () => backend.settingsPort(),
45
45
  });
@@ -72,18 +72,12 @@ export async function composeOwnedUi(options = {}) {
72
72
  ...(options.packageVersion === undefined ? {} : { packageVersion: options.packageVersion }),
73
73
  ...(options.profileId === undefined ? {} : { profileId: options.profileId }),
74
74
  });
75
- const unsubscribeAccentChange = bareSettings === null
76
- ? () => { }
77
- : bareSettings.onChange(current => {
78
- const accent = current.value("accentColor");
79
- host.setAccentColor(accent);
80
- });
81
- // Rationale: separate A1 processes share one settings document; a live scope change saved in
82
- // one reaches the others only by rereading that document.
75
+ const sessionColor = bindSessionColor(backend, bareSettings, color => host.setAccentColor(color), productPaths.runtimeDir);
76
+ // Rationale: peer refresh shares persisted preferences, never session-local Current color.
83
77
  const unwatchSharedSettings = bareSettings === null
84
78
  ? () => { }
85
79
  : watchSharedSettings(bareSettings);
86
- const unsubscribeAccent = () => { unwatchSharedSettings(); unsubscribeAccentChange(); };
80
+ const unsubscribeAccent = async () => { unwatchSharedSettings(); await sessionColor.dispose(); };
87
81
  // Invariant: the Settings search and agent prompt borrow the same resolved Pi keybindings.
88
82
  const keybindingsHost = createPiKeybindingsHost({ profile: bare ? "a1" : "pi", agentDir: backend.identity.agentDir });
89
83
  // Rationale: Routes open only after shell construction.
@@ -118,7 +112,7 @@ export async function composeOwnedUi(options = {}) {
118
112
  const releaseShared = async () => {
119
113
  naming.dispose();
120
114
  clipboardServices?.dispose();
121
- unsubscribeAccent();
115
+ await unsubscribeAccent();
122
116
  await sessionRuntimes.dispose();
123
117
  await releaseNote.releaseClaim();
124
118
  suggestionDiagnostics?.dispose();
@@ -184,6 +178,7 @@ export async function composeOwnedUi(options = {}) {
184
178
  exitNotice = import("../app/session-shell/exit-notice.js")
185
179
  .then(module => module.armExitNotice(backend, exitNoticePath), () => null);
186
180
  presenter.start();
181
+ sessionColor.start();
187
182
  releaseUpdates.start();
188
183
  },
189
184
  flush: () => backend.session.flushEvents(),
@@ -204,6 +199,31 @@ export async function composeOwnedUi(options = {}) {
204
199
  };
205
200
  return { application, settings };
206
201
  }
202
+ function bindSessionColor(backend, settings, apply, runtimeDir) {
203
+ if (settings === null)
204
+ return { start() { }, async dispose() { } };
205
+ let control = Promise.resolve(null);
206
+ let started = false, stopped = false;
207
+ const unsubscribe = settings.onChange(current => apply(current.value("accentColor")));
208
+ let binding = backend.identity.sessionBindingGeneration;
209
+ const unbind = backend.session.onEvent(() => {
210
+ if (binding === backend.identity.sessionBindingGeneration)
211
+ return;
212
+ binding = backend.identity.sessionBindingGeneration;
213
+ settings.startSession();
214
+ });
215
+ return {
216
+ start() {
217
+ if (started || stopped)
218
+ return;
219
+ started = true;
220
+ control = import("./session-colors.js").then(module => stopped ? null
221
+ : module.startSessionColorControl(backend, settings, runtimeDir, process.env[PRODUCT_IDENTITY.environment.sessionRuntimeId]))
222
+ .catch(() => null);
223
+ },
224
+ async dispose() { stopped = true; unbind(); unsubscribe(); await (await control)?.dispose(); },
225
+ };
226
+ }
207
227
  function spinnerPresenterOptions(settings) {
208
228
  return settings === null ? {} : { spinner: {
209
229
  snapshot: () => settings.value("spinnerStyle"),
@@ -0,0 +1,5 @@
1
+ import { type OwnedUiSessionBackend } from "../contracts/owned-ui/index.js";
2
+ import type { OwnedSettingsManager } from "../ui/settings/manager.js";
3
+ export declare function startSessionColorControl(backend: OwnedUiSessionBackend, settings: OwnedSettingsManager, runtimeDir: string, runtimeId?: string): Promise<{
4
+ dispose: () => Promise<void>;
5
+ }>;
@@ -0,0 +1,33 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { UI_ACCENT_COLORS } from "../contracts/owned-ui/index.js";
3
+ import { serveSessionControl } from "../foundation/lifecycle/session-control.js";
4
+ export async function startSessionColorControl(backend, settings, runtimeDir, runtimeId = randomUUID()) {
5
+ const identity = () => ({ sessionId: backend.identity.currentHistorySessionId() ?? backend.identity.sessionId, binding: backend.identity.sessionBindingGeneration });
6
+ const control = await serveSessionControl({ runtimeDir, runtimeId, identity, handle: async (payload) => {
7
+ if (backend.identity.disposed)
8
+ throw Object.assign(new Error("The session is stopped."), { code: "session-unavailable" });
9
+ if (!payload || typeof payload !== "object")
10
+ throw Object.assign(new Error("Invalid color request."), { code: "invalid-request" });
11
+ const request = payload;
12
+ const sessionId = identity().sessionId;
13
+ if (request.action === "set") {
14
+ if (!UI_ACCENT_COLORS.includes(request.color))
15
+ throw Object.assign(new Error("Unknown color."), { code: "invalid-color" });
16
+ const result = await settings.change("a1", "accentColor", request.color);
17
+ if (!result.applied)
18
+ throw Object.assign(new Error(result.failure ?? "Color could not be applied."), { code: "control-failed" });
19
+ return { sessionId, color: result.effectiveValue };
20
+ }
21
+ if (request.action !== "get")
22
+ throw Object.assign(new Error("Unknown color action."), { code: "invalid-request" });
23
+ return { sessionId, color: settings.value("accentColor") };
24
+ } });
25
+ let stopped = false, binding = backend.identity.sessionBindingGeneration, publication = Promise.resolve();
26
+ const unsubscribe = backend.session.onEvent(() => {
27
+ if (stopped || binding === backend.identity.sessionBindingGeneration)
28
+ return;
29
+ binding = backend.identity.sessionBindingGeneration;
30
+ publication = publication.then(() => control.publish()).catch(() => { });
31
+ });
32
+ return { dispose: async () => { stopped = true; unsubscribe(); await publication; await control.dispose(); } };
33
+ }
@@ -17,7 +17,9 @@ export declare class SessionRuntimeRegistry {
17
17
  revision: string;
18
18
  } | null>;
19
19
  /** Strict tool cwd lookup: transient startup failure never redirects a write to primary. */
20
- toolReaderFor(agentId: string | undefined): (sessionId: string, sessionFile: string, signal: AbortSignal) => Promise<string>;
20
+ toolReaderFor(agentId: string | undefined): ((sessionId: string, sessionFile: string, signal: AbortSignal) => Promise<string>) & {
21
+ settleShell: (sessionId: string, sessionFile: string, signal: AbortSignal) => Promise<string | null>;
22
+ };
21
23
  /** Drain publication before releasing claims; never republish after disposal. */
22
24
  dispose(): Promise<void>;
23
25
  }
@@ -3,6 +3,8 @@ export class SessionRuntimeRegistry {
3
3
  #options;
4
4
  #identities = new Map();
5
5
  #activated = new Map();
6
+ // Invariant: a released invalid attachment is reported by the agent's next tool call instead of running elsewhere.
7
+ #released = new Map();
6
8
  #abort = new AbortController();
7
9
  #owner = null;
8
10
  #timer = null;
@@ -26,7 +28,7 @@ export class SessionRuntimeRegistry {
26
28
  const owner = await this.#ownerIdentity();
27
29
  if (owner === null || !this.#current(agentId, identity))
28
30
  return null;
29
- const { activateSessionRepositoryContext, readActiveSessionRepositoryContext } = await import("../foundation/lifecycle/session-repository-context.js");
31
+ const { activateSessionRepositoryContext, readActiveSessionRepositoryContext, releaseInvalidSessionRepositoryContext } = await import("../foundation/lifecycle/session-repository-context.js");
30
32
  if (!this.#current(agentId, identity))
31
33
  return null;
32
34
  const options = {
@@ -51,6 +53,14 @@ export class SessionRuntimeRegistry {
51
53
  }
52
54
  catch (error) {
53
55
  const code = error instanceof Error && "code" in error ? error.code : null;
56
+ if (code === "worktree-attachment-invalid") {
57
+ const released = await releaseInvalidSessionRepositoryContext(identity, options);
58
+ if (released !== null && this.#current(agentId, identity)) {
59
+ this.#released.set(agentId, { identity, message: `attached worktree ${released.worktreeRoot} is no longer a valid Git worktree; `
60
+ + `this session was detached and now uses ${released.startupRoot}. Nothing was deleted and this call did not run; retry it from that directory.` });
61
+ return null;
62
+ }
63
+ }
54
64
  if ((code === "session-runtime-unavailable" || code === "worktree-context-changed") && this.#current(agentId, identity)) {
55
65
  this.#activated.delete(agentId);
56
66
  }
@@ -62,7 +72,18 @@ export class SessionRuntimeRegistry {
62
72
  /** Strict tool cwd lookup: transient startup failure never redirects a write to primary. */
63
73
  toolReaderFor(agentId) {
64
74
  const read = this.readerFor(agentId);
65
- return async (sessionId, sessionFile, signal) => {
75
+ /** Run a completion the just-finished shell requested, from this stable process rather than that shell. */
76
+ const settleShell = async (sessionId, sessionFile, signal) => {
77
+ const { runtimeId, inspectProcess } = this.#options;
78
+ if (this.#disposed || runtimeId === undefined || inspectProcess === undefined)
79
+ return null;
80
+ const { describeWorktreeCompletion, settleSessionWorktreeCompletion } = await import("../foundation/lifecycle/session-worktree-completion.js");
81
+ const result = await settleSessionWorktreeCompletion({ sessionId, sessionFile }, {
82
+ runtimeId, inspectProcess, signal, ...(agentId === undefined ? {} : { agentId }),
83
+ });
84
+ return result === null ? null : describeWorktreeCompletion(result);
85
+ };
86
+ return Object.assign(async (sessionId, sessionFile, signal) => {
66
87
  const { retrySessionCoordination } = await import("../foundation/lifecycle/session-context-retry.js");
67
88
  return await retrySessionCoordination(async () => {
68
89
  signal.throwIfAborted();
@@ -75,12 +96,20 @@ export class SessionRuntimeRegistry {
75
96
  if (active?.sessionId !== sessionId || active.sessionFile !== sessionFile) {
76
97
  throw Object.assign(new Error("worktree runtime registration is still recovering"), { code: "session-runtime-unavailable" });
77
98
  }
99
+ const released = this.#released.get(agentId);
100
+ if (released !== undefined) {
101
+ this.#released.delete(agentId);
102
+ // Invariant: work addressed to the lost checkout fails once; it is never silently applied to the startup checkout.
103
+ if (released.identity.sessionId === sessionId && released.identity.sessionFile === sessionFile) {
104
+ throw Object.assign(new Error(released.message), { code: "worktree-attachment-invalid" });
105
+ }
106
+ }
78
107
  if (context !== null)
79
108
  return context.cwd;
80
109
  const { readSessionStartupRoot } = await import("../foundation/lifecycle/session-repository-context.js");
81
110
  return await readSessionStartupRoot({ sessionId, sessionFile });
82
111
  }, 8_000, signal);
83
- };
112
+ }, { settleShell });
84
113
  }
85
114
  /** Drain publication before releasing claims; never republish after disposal. */
86
115
  async dispose() {
@@ -1,6 +1,6 @@
1
1
  export type { OwnedUiSessionNameState, OwnedUiSessionNameCompletion, OwnedUiSessionNamingPort } from "./session-naming.js";
2
- /** Stable stored choices for bare A1's semantic UI accent. */
3
- export type UiAccentColor = "purple" | "blue" | "cyan" | "green" | "orange" | "pink";
2
+ export declare const UI_ACCENT_COLORS: readonly ["blue", "green", "orange", "pink", "purple", "cyan", "grey"];
3
+ export type UiAccentColor = typeof UI_ACCENT_COLORS[number];
4
4
  export { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiExtensionUiPort, } from "./extension-ui.js";
5
5
  export type { OwnedUiExtensionAutocompleteItem, OwnedUiExtensionAutocompleteProvider, OwnedUiExtensionAutocompleteProviderFactory, OwnedUiExtensionAutocompleteSuggestions, OwnedUiExtensionComponent, OwnedUiExtensionComponentFactory, OwnedUiExtensionComponentFactoryContext, OwnedUiExtensionCustomFactory, OwnedUiExtensionCustomOptions, OwnedUiExtensionDialogOptions, OwnedUiExtensionEditorComponent, OwnedUiExtensionEditorFactory, OwnedUiExtensionEditorThemePort, OwnedUiExtensionEntryRenderInput, OwnedUiExtensionFooterDataPort, OwnedUiExtensionFooterFactory, OwnedUiExtensionInputListener, OwnedUiExtensionKeybindingsPort, OwnedUiExtensionMessageRenderInput, OwnedUiExtensionNotificationSeverity, OwnedUiExtensionOverlayAnchor, OwnedUiExtensionOverlayHandle, OwnedUiExtensionOverlayMargin, OwnedUiExtensionOverlayOptions, OwnedUiExtensionRenderCallback, OwnedUiExtensionRendererPort, OwnedUiExtensionRuntimePort, OwnedUiExtensionSelectListThemePort, OwnedUiExtensionSizeValue, OwnedUiExtensionTerminalPort, OwnedUiExtensionThemeBackground, OwnedUiExtensionThemeColor, OwnedUiExtensionThemeInfo, OwnedUiExtensionThemePort, OwnedUiExtensionToolRenderContext, OwnedUiExtensionUiCallback, OwnedUiExtensionUiPort, OwnedUiExtensionUiProperty, OwnedUiExtensionWidgetOptions, OwnedUiExtensionWidgetPlacement, OwnedUiExtensionWorkingIndicatorOptions, } from "./extension-ui.js";
6
6
  export { OWNED_UI_CONTRACT_VERSION } from "./model.js";
@@ -1,3 +1,4 @@
1
+ export const UI_ACCENT_COLORS = Object.freeze(["blue", "green", "orange", "pink", "purple", "cyan", "grey"]);
1
2
  export { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiExtensionUiPort, } from "./extension-ui.js";
2
3
  export { OWNED_UI_CONTRACT_VERSION } from "./model.js";
3
4
  export { OWNED_UI_HIDDEN_COMMAND_NAMES, OWNED_UI_MODELS_COMMAND_NAME, OWNED_UI_WORKFLOW_COMMAND_NAMES, } from "./session-workflows.js";
@@ -29,7 +29,7 @@ export declare const SHARED_PROGRESS_PALETTE: Readonly<{
29
29
  };
30
30
  };
31
31
  };
32
- purple: {
32
+ green: {
33
33
  dark: {
34
34
  truecolor: {
35
35
  accent: string;
@@ -55,7 +55,7 @@ export declare const SHARED_PROGRESS_PALETTE: Readonly<{
55
55
  };
56
56
  };
57
57
  };
58
- cyan: {
58
+ orange: {
59
59
  dark: {
60
60
  truecolor: {
61
61
  accent: string;
@@ -81,7 +81,7 @@ export declare const SHARED_PROGRESS_PALETTE: Readonly<{
81
81
  };
82
82
  };
83
83
  };
84
- green: {
84
+ pink: {
85
85
  dark: {
86
86
  truecolor: {
87
87
  accent: string;
@@ -107,7 +107,7 @@ export declare const SHARED_PROGRESS_PALETTE: Readonly<{
107
107
  };
108
108
  };
109
109
  };
110
- orange: {
110
+ purple: {
111
111
  dark: {
112
112
  truecolor: {
113
113
  accent: string;
@@ -133,7 +133,33 @@ export declare const SHARED_PROGRESS_PALETTE: Readonly<{
133
133
  };
134
134
  };
135
135
  };
136
- pink: {
136
+ cyan: {
137
+ dark: {
138
+ truecolor: {
139
+ accent: string;
140
+ border: string;
141
+ selectedBg: string;
142
+ };
143
+ "256color": {
144
+ accent: string;
145
+ border: string;
146
+ selectedBg: string;
147
+ };
148
+ };
149
+ light: {
150
+ truecolor: {
151
+ accent: string;
152
+ border: string;
153
+ selectedBg: string;
154
+ };
155
+ "256color": {
156
+ accent: string;
157
+ border: string;
158
+ selectedBg: string;
159
+ };
160
+ };
161
+ };
162
+ grey: {
137
163
  dark: {
138
164
  truecolor: {
139
165
  accent: string;
@@ -2,5 +2,5 @@
2
2
  * Generated by scripts/pi/sync-progress-palette.ts from A1's named accent projection
3
3
  * over pinned Pi 1.5.2 (MIT), commit b4afdddf44e059c67ccf5097e5317d913521b470. Do not edit directly.
4
4
  */
5
- export const SHARED_PROGRESS_PALETTE = Object.freeze({ "blue": { "dark": { "truecolor": { "accent": "\u001b[38;2;130;160;241m", "border": "\u001b[38;2;116;102;197m", "selectedBg": "\u001b[48;2;52;57;69m" }, "256color": { "accent": "\u001b[38;5;111m", "border": "\u001b[38;5;98m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;63;96;226m", "border": "\u001b[38;2;83;67;154m", "selectedBg": "\u001b[48;2;243;244;246m" }, "256color": { "accent": "\u001b[38;5;62m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } }, "purple": { "dark": { "truecolor": { "accent": "\u001b[38;2;167;152;215m", "border": "\u001b[38;2;105;115;167m", "selectedBg": "\u001b[48;2;58;56;66m" }, "256color": { "accent": "\u001b[38;5;140m", "border": "\u001b[38;5;61m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;116;89;180m", "border": "\u001b[38;2;68;77;130m", "selectedBg": "\u001b[48;2;243;243;245m" }, "256color": { "accent": "\u001b[38;5;97m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } }, "cyan": { "dark": { "truecolor": { "accent": "\u001b[38;2;93;179;186m", "border": "\u001b[38;2;74;128;146m", "selectedBg": "\u001b[48;2;48;60;61m" }, "256color": { "accent": "\u001b[38;5;73m", "border": "\u001b[38;5;66m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;40;122;129m", "border": "\u001b[38;2;40;89;106m", "selectedBg": "\u001b[48;2;241;244;244m" }, "256color": { "accent": "\u001b[38;5;30m", "border": "\u001b[38;5;23m", "selectedBg": "\u001b[48;5;255m" } } }, "green": { "dark": { "truecolor": { "accent": "\u001b[38;2;104;183;141m", "border": "\u001b[38;2;75;132;120m", "selectedBg": "\u001b[48;2;50;60;54m" }, "256color": { "accent": "\u001b[38;5;72m", "border": "\u001b[38;5;66m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;51;126;88m", "border": "\u001b[38;2;41;93;82m", "selectedBg": "\u001b[48;2;242;244;243m" }, "256color": { "accent": "\u001b[38;5;65m", "border": "\u001b[38;5;23m", "selectedBg": "\u001b[48;5;255m" } } }, "orange": { "dark": { "truecolor": { "accent": "\u001b[38;2;231;135;83m", "border": "\u001b[38;2;178;89;79m", "selectedBg": "\u001b[48;2;67;54;48m" }, "256color": { "accent": "\u001b[38;5;173m", "border": "\u001b[38;5;131m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;174;80;16m", "border": "\u001b[38;2;133;57;49m", "selectedBg": "\u001b[48;2;245;243;242m" }, "256color": { "accent": "\u001b[38;5;130m", "border": "\u001b[38;5;95m", "selectedBg": "\u001b[48;5;255m" } } }, "pink": { "dark": { "truecolor": { "accent": "\u001b[38;2;223;122;199m", "border": "\u001b[38;2;150;91;171m", "selectedBg": "\u001b[48;2;65;53;62m" }, "256color": { "accent": "\u001b[38;5;176m", "border": "\u001b[38;5;97m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;178;56;153m", "border": "\u001b[38;2;114;61;131m", "selectedBg": "\u001b[48;2;245;243;245m" }, "256color": { "accent": "\u001b[38;5;132m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } } });
5
+ export const SHARED_PROGRESS_PALETTE = Object.freeze({ "blue": { "dark": { "truecolor": { "accent": "\u001b[38;2;130;160;241m", "border": "\u001b[38;2;116;102;197m", "selectedBg": "\u001b[48;2;52;57;69m" }, "256color": { "accent": "\u001b[38;5;111m", "border": "\u001b[38;5;98m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;63;96;226m", "border": "\u001b[38;2;83;67;154m", "selectedBg": "\u001b[48;2;243;244;246m" }, "256color": { "accent": "\u001b[38;5;62m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } }, "green": { "dark": { "truecolor": { "accent": "\u001b[38;2;104;183;141m", "border": "\u001b[38;2;75;132;120m", "selectedBg": "\u001b[48;2;50;60;54m" }, "256color": { "accent": "\u001b[38;5;72m", "border": "\u001b[38;5;66m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;51;126;88m", "border": "\u001b[38;2;41;93;82m", "selectedBg": "\u001b[48;2;242;244;243m" }, "256color": { "accent": "\u001b[38;5;65m", "border": "\u001b[38;5;23m", "selectedBg": "\u001b[48;5;255m" } } }, "orange": { "dark": { "truecolor": { "accent": "\u001b[38;2;231;135;83m", "border": "\u001b[38;2;178;89;79m", "selectedBg": "\u001b[48;2;67;54;48m" }, "256color": { "accent": "\u001b[38;5;173m", "border": "\u001b[38;5;131m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;174;80;16m", "border": "\u001b[38;2;133;57;49m", "selectedBg": "\u001b[48;2;245;243;242m" }, "256color": { "accent": "\u001b[38;5;130m", "border": "\u001b[38;5;95m", "selectedBg": "\u001b[48;5;255m" } } }, "pink": { "dark": { "truecolor": { "accent": "\u001b[38;2;223;122;199m", "border": "\u001b[38;2;150;91;171m", "selectedBg": "\u001b[48;2;65;53;62m" }, "256color": { "accent": "\u001b[38;5;176m", "border": "\u001b[38;5;97m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;178;56;153m", "border": "\u001b[38;2;114;61;131m", "selectedBg": "\u001b[48;2;245;243;245m" }, "256color": { "accent": "\u001b[38;5;132m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } }, "purple": { "dark": { "truecolor": { "accent": "\u001b[38;2;167;152;215m", "border": "\u001b[38;2;105;115;167m", "selectedBg": "\u001b[48;2;58;56;66m" }, "256color": { "accent": "\u001b[38;5;140m", "border": "\u001b[38;5;61m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;116;89;180m", "border": "\u001b[38;2;68;77;130m", "selectedBg": "\u001b[48;2;243;243;245m" }, "256color": { "accent": "\u001b[38;5;97m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } }, "cyan": { "dark": { "truecolor": { "accent": "\u001b[38;2;93;179;186m", "border": "\u001b[38;2;74;128;146m", "selectedBg": "\u001b[48;2;48;60;61m" }, "256color": { "accent": "\u001b[38;5;73m", "border": "\u001b[38;5;66m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;40;122;129m", "border": "\u001b[38;2;40;89;106m", "selectedBg": "\u001b[48;2;241;244;244m" }, "256color": { "accent": "\u001b[38;5;30m", "border": "\u001b[38;5;23m", "selectedBg": "\u001b[48;5;255m" } } }, "grey": { "dark": { "truecolor": { "accent": "\u001b[38;2;163;163;163m", "border": "\u001b[38;2;119;119;119m", "selectedBg": "\u001b[48;2;57;57;57m" }, "256color": { "accent": "\u001b[38;5;247m", "border": "\u001b[38;5;243m", "selectedBg": "\u001b[48;5;237m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;111;111;111m", "border": "\u001b[38;2;84;84;84m", "selectedBg": "\u001b[48;2;244;244;244m" }, "256color": { "accent": "\u001b[38;5;242m", "border": "\u001b[38;5;240m", "selectedBg": "\u001b[48;5;255m" } } } });
6
6
  export const SHARED_PROGRESS_ACCENT_ANSI = SHARED_PROGRESS_PALETTE.blue.dark.truecolor.accent;
@@ -9,6 +9,10 @@ export type SessionContextRequest = {
9
9
  readonly base?: string;
10
10
  } | {
11
11
  readonly action: "unlink-worktree";
12
+ } | {
13
+ readonly action: "complete-worktree";
14
+ readonly change?: string;
15
+ readonly pr?: number;
12
16
  } | {
13
17
  readonly action: "worktrees";
14
18
  };
@@ -18,6 +22,11 @@ export type SessionContextOutcome = {
18
22
  readonly branch: string;
19
23
  } | {
20
24
  readonly kind: "unlinked";
25
+ } | {
26
+ readonly kind: "completing";
27
+ readonly cwd: string;
28
+ readonly branch: string;
29
+ readonly startupRoot: string;
21
30
  } | {
22
31
  readonly kind: "worktrees";
23
32
  readonly entries: readonly SessionWorktreeInventoryEntry[];
@@ -1,4 +1,4 @@
1
- import { clearSessionRepositoryContext, enterSessionRepositoryWorktree, listSessionRepositoryWorktrees, retrySessionCoordination, setSessionRepositoryContext, } from "../../foundation/lifecycle/index.js";
1
+ import { clearSessionRepositoryContext, enterSessionRepositoryWorktree, listSessionRepositoryWorktrees, requestSessionWorktreeCompletion, retrySessionCoordination, setSessionRepositoryContext, } from "../../foundation/lifecycle/index.js";
2
2
  export { PRIMARY_SESSION_AGENT_ID } from "../../foundation/lifecycle/index.js";
3
3
  export async function updateSessionRepositoryContext(identity, request, runtime) {
4
4
  const options = {
@@ -14,6 +14,12 @@ export async function updateSessionRepositoryContext(identity, request, runtime)
14
14
  await clearSessionRepositoryContext(identity, options);
15
15
  return { kind: "unlinked" };
16
16
  }
17
+ if (request.action === "complete-worktree") {
18
+ return { kind: "completing", ...await requestSessionWorktreeCompletion(identity, {
19
+ ...(request.change === undefined ? {} : { change: request.change }),
20
+ ...(request.pr === undefined ? {} : { pr: request.pr }),
21
+ }, options) };
22
+ }
17
23
  if (request.action === "worktrees") {
18
24
  return { kind: "worktrees", entries: await listSessionRepositoryWorktrees(identity, options) };
19
25
  }
@@ -732,7 +732,7 @@ export class SettingsApp {
732
732
  const state = {
733
733
  choices: menu.choices.map(choice => displaySettingValue(menu.entry, choice)),
734
734
  ...(menu.preview !== null ? { previews: menu.preview.frames(menu.choices, menu.index, theme) }
735
- : menu.entry.id === "accentColor" ? {
735
+ : ["accentColor", "defaultAccentColor"].includes(menu.entry.id) ? {
736
736
  previews: menu.choices.map(choice => theme.accentPreview?.(String(choice), "■") ?? ""),
737
737
  } : {}),
738
738
  current: this.#edits.shown(menu.entry) === null ? null : displaySettingValue(menu.entry, this.#edits.shown(menu.entry)),