@timurproko/a1 0.2.7-dev.1265 → 0.2.7-dev.1268
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/README.md +2 -0
- package/dist/app/session-shell/session-name-reveal.d.ts +17 -0
- package/dist/app/session-shell/session-name-reveal.js +49 -0
- package/dist/app/session-shell/session-shell-root.d.ts +2 -1
- package/dist/app/session-shell/session-shell-root.js +14 -9
- package/dist/app/session-shell/session-shell.js +6 -0
- package/dist/app/session-shell/session-terminal-presentation.d.ts +7 -0
- package/dist/app/session-shell/session-terminal-presentation.js +11 -1
- package/dist/app/session-shell/shell-host.js +3 -1
- package/dist/composition/owned-ui.js +11 -7
- package/dist/composition/session-backend.d.ts +2 -1
- package/dist/composition/session-naming.d.ts +6 -0
- package/dist/composition/session-naming.js +22 -0
- package/dist/composition/session-runtime-registry.d.ts +5 -1
- package/dist/composition/session-runtime-registry.js +11 -3
- package/dist/contracts/owned-ui/index.d.ts +1 -0
- package/dist/contracts/owned-ui/model.d.ts +6 -0
- package/dist/contracts/owned-ui/session-naming.d.ts +22 -0
- package/dist/contracts/owned-ui/session-naming.js +1 -0
- package/dist/contracts/owned-ui/shell-components.d.ts +5 -0
- package/dist/contracts/owned-ui/validation.js +5 -0
- package/dist/features/session-naming/controller.d.ts +21 -0
- package/dist/features/session-naming/controller.js +148 -0
- package/dist/features/session-naming/generate.d.ts +3 -0
- package/dist/features/session-naming/generate.js +44 -0
- package/dist/features/session-naming/index.d.ts +2 -0
- package/dist/features/session-naming/index.js +1 -0
- package/dist/foundation/lifecycle/index.d.ts +2 -2
- package/dist/foundation/lifecycle/index.js +1 -1
- package/dist/foundation/lifecycle/session-repository-context.d.ts +12 -0
- package/dist/foundation/lifecycle/session-repository-context.js +59 -3
- package/dist/integrations/pi/components/shell-footer-status.js +1 -0
- package/dist/integrations/pi/components/shell-presenters-transcript.js +3 -6
- package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +5 -0
- package/dist/integrations/pi/components/upstream/components/session-footer.js +44 -7
- package/dist/integrations/pi/components/upstream/components/tool-execution.js +4 -8
- package/dist/integrations/pi/engine/adapter.d.ts +3 -2
- package/dist/integrations/pi/engine/adapter.js +37 -28
- package/dist/integrations/pi/engine/repository-metadata.d.ts +2 -0
- package/dist/integrations/pi/engine/repository-metadata.js +12 -16
- package/dist/integrations/pi/engine/session-events.d.ts +1 -0
- package/dist/integrations/pi/engine/session-events.js +2 -0
- package/dist/integrations/pi/engine/session-naming.d.ts +24 -0
- package/dist/integrations/pi/engine/session-naming.js +88 -0
- package/dist/integrations/pi/engine/view-state.d.ts +5 -1
- package/dist/integrations/pi/engine/view-state.js +3 -1
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/session-name-frame.d.ts +17 -0
- package/dist/ui/components/session-name-frame.js +24 -0
- package/dist/ui/settings/declarations.d.ts +12 -0
- package/dist/ui/settings/declarations.js +9 -0
- package/docs/architecture/project-structure.md +1 -0
- package/docs/architecture/tool-image-presentation.md +3 -3
- package/docs/features/session-naming.md +23 -0
- package/package.json +3 -3
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { generateSessionName } from "./generate.js";
|
|
2
|
+
function contextKey(context) {
|
|
3
|
+
return JSON.stringify([context.cwd.replace(/\\/g, "/"), context.branch, context.revision]);
|
|
4
|
+
}
|
|
5
|
+
/** One background naming chain per eligible prompt/context; owns no timers or engine objects. */
|
|
6
|
+
export class SessionNamingController {
|
|
7
|
+
#options;
|
|
8
|
+
#unsubscribe;
|
|
9
|
+
#binding;
|
|
10
|
+
#seen = new Set();
|
|
11
|
+
#signature;
|
|
12
|
+
#epoch = 0;
|
|
13
|
+
#run;
|
|
14
|
+
#work;
|
|
15
|
+
#attempted;
|
|
16
|
+
#disposed = false;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.#options = options;
|
|
19
|
+
this.#binding = options.backend.identity.sessionBindingGeneration;
|
|
20
|
+
this.#rememberTranscript();
|
|
21
|
+
this.#unsubscribe = options.backend.session.onEvent(event => this.#event(event));
|
|
22
|
+
}
|
|
23
|
+
settingsChanged() { if (!this.#options.enabled())
|
|
24
|
+
this.#invalidate(); }
|
|
25
|
+
dispose() {
|
|
26
|
+
if (this.#disposed)
|
|
27
|
+
return;
|
|
28
|
+
this.#disposed = true;
|
|
29
|
+
this.#unsubscribe();
|
|
30
|
+
this.#invalidate();
|
|
31
|
+
}
|
|
32
|
+
#invalidate() {
|
|
33
|
+
this.#epoch++;
|
|
34
|
+
this.#attempted = undefined;
|
|
35
|
+
this.#run?.abort.abort();
|
|
36
|
+
this.#run = undefined;
|
|
37
|
+
this.#work?.abort.abort();
|
|
38
|
+
this.#work = undefined;
|
|
39
|
+
}
|
|
40
|
+
#rememberTranscript() {
|
|
41
|
+
this.#seen = new Set(this.#options.backend.session.view().transcript.filter(block => block.kind === "user").map(block => block.id));
|
|
42
|
+
}
|
|
43
|
+
#event(event) {
|
|
44
|
+
if (this.#disposed)
|
|
45
|
+
return;
|
|
46
|
+
const { backend } = this.#options;
|
|
47
|
+
if (this.#binding !== backend.identity.sessionBindingGeneration) {
|
|
48
|
+
this.#binding = backend.identity.sessionBindingGeneration;
|
|
49
|
+
this.#invalidate();
|
|
50
|
+
this.#attempted = undefined;
|
|
51
|
+
this.#signature = undefined;
|
|
52
|
+
this.#rememberTranscript();
|
|
53
|
+
}
|
|
54
|
+
if (event.type === "session-view") {
|
|
55
|
+
const footer = event.view.status.footer;
|
|
56
|
+
const signature = JSON.stringify([footer?.repositoryPath, footer?.branch, footer?.repositoryRevision]);
|
|
57
|
+
if (this.#signature !== undefined && signature !== this.#signature)
|
|
58
|
+
this.#checkContext();
|
|
59
|
+
this.#signature = signature;
|
|
60
|
+
if (this.#run)
|
|
61
|
+
for (const block of event.view.transcript)
|
|
62
|
+
if (block.kind === "user")
|
|
63
|
+
this.#prompt(block.id, block.text);
|
|
64
|
+
}
|
|
65
|
+
if (event.type === "agent-run-started") {
|
|
66
|
+
this.#run?.abort.abort();
|
|
67
|
+
const run = { epoch: this.#epoch, abort: new AbortController() };
|
|
68
|
+
this.#run = run;
|
|
69
|
+
if (!this.#options.enabled())
|
|
70
|
+
return;
|
|
71
|
+
const state = this.#options.naming.state();
|
|
72
|
+
if (!state)
|
|
73
|
+
return;
|
|
74
|
+
run.state = state;
|
|
75
|
+
void this.#options.withContext(context => context, run.abort.signal).then(context => {
|
|
76
|
+
if (this.#run !== run || run.epoch !== this.#epoch || run.abort.signal.aborted
|
|
77
|
+
|| state.binding !== backend.identity.sessionBindingGeneration)
|
|
78
|
+
return;
|
|
79
|
+
run.context = context;
|
|
80
|
+
if (this.#work && this.#work.key !== contextKey(context)) {
|
|
81
|
+
this.#work.abort.abort();
|
|
82
|
+
this.#work = undefined;
|
|
83
|
+
}
|
|
84
|
+
this.#launch(run);
|
|
85
|
+
}).catch(() => { });
|
|
86
|
+
for (const block of backend.session.view().transcript)
|
|
87
|
+
if (block.kind === "user")
|
|
88
|
+
this.#prompt(block.id, block.text);
|
|
89
|
+
}
|
|
90
|
+
if (event.type === "transcript-block" && event.block.kind === "user")
|
|
91
|
+
this.#prompt(event.block.id, event.block.text);
|
|
92
|
+
if (event.type === "agent-run-settled" && this.#run?.prompt === undefined) {
|
|
93
|
+
this.#run?.abort.abort();
|
|
94
|
+
this.#run = undefined;
|
|
95
|
+
}
|
|
96
|
+
if (event.type === "session-lifecycle" && ["stopping", "stopped", "failed"].includes(event.lifecycle))
|
|
97
|
+
this.#invalidate();
|
|
98
|
+
}
|
|
99
|
+
#checkContext() {
|
|
100
|
+
const run = this.#run;
|
|
101
|
+
if (!run)
|
|
102
|
+
return;
|
|
103
|
+
run.checking = true;
|
|
104
|
+
void this.#options.withContext(context => context, run.abort.signal).then(current => {
|
|
105
|
+
if (this.#run !== run || run.abort.signal.aborted || this.#disposed)
|
|
106
|
+
return;
|
|
107
|
+
if (run.context && contextKey(current) !== contextKey(run.context)) {
|
|
108
|
+
this.#invalidate();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
run.checking = false;
|
|
112
|
+
this.#launch(run);
|
|
113
|
+
}).catch(() => { });
|
|
114
|
+
}
|
|
115
|
+
#prompt(id, text) {
|
|
116
|
+
if (this.#seen.has(id))
|
|
117
|
+
return;
|
|
118
|
+
this.#seen.add(id);
|
|
119
|
+
const run = this.#run;
|
|
120
|
+
if (!run || !text.trim() || text.trimStart().startsWith("/"))
|
|
121
|
+
return;
|
|
122
|
+
run.prompt = text.trim();
|
|
123
|
+
this.#launch(run);
|
|
124
|
+
}
|
|
125
|
+
#launch(run) {
|
|
126
|
+
const { context, state, prompt } = run;
|
|
127
|
+
if (!context || !state || !prompt || run.checking || this.#disposed || !this.#options.enabled()
|
|
128
|
+
|| run.abort.signal.aborted || run.epoch !== this.#epoch || state.source === "user")
|
|
129
|
+
return;
|
|
130
|
+
const repositoryKey = contextKey(context);
|
|
131
|
+
const key = JSON.stringify([state.sessionFile, repositoryKey]);
|
|
132
|
+
if (state.completedContext === key || this.#attempted === key || this.#work)
|
|
133
|
+
return;
|
|
134
|
+
this.#attempted = key;
|
|
135
|
+
const work = new AbortController();
|
|
136
|
+
this.#work = { abort: work, key: repositoryKey };
|
|
137
|
+
void generateSessionName(prompt, `${context.cwd.split(/[\\/]/).at(-1) ?? ""} (${context.branch ?? "no branch"})`, request => this.#options.naming.complete(request), work.signal).then(name => {
|
|
138
|
+
if (work.signal.aborted || this.#disposed || !this.#options.enabled())
|
|
139
|
+
return;
|
|
140
|
+
return this.#options.withContext(current => {
|
|
141
|
+
if (contextKey(current) !== repositoryKey || work.signal.aborted || this.#disposed || !this.#options.enabled())
|
|
142
|
+
return false;
|
|
143
|
+
return this.#options.naming.apply(state, name, key);
|
|
144
|
+
}, work.signal);
|
|
145
|
+
}).catch(() => { }).finally(() => { if (this.#work?.abort === work)
|
|
146
|
+
this.#work = undefined; });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { OwnedUiSessionNameCompletion } from "../../contracts/owned-ui/index.js";
|
|
2
|
+
/** Two bounded tool-free attempts; cancellation never becomes an automatic fallback. */
|
|
3
|
+
export declare function generateSessionName(prompt: string, hints: string, complete: (request: OwnedUiSessionNameCompletion) => Promise<string | null>, signal: AbortSignal, timeoutMs?: number): Promise<string>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function normalize(value) {
|
|
2
|
+
const name = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3
|
+
if (!name)
|
|
4
|
+
return null;
|
|
5
|
+
if (name.length <= 16)
|
|
6
|
+
return name;
|
|
7
|
+
const boundary = name.slice(0, 17).lastIndexOf("-");
|
|
8
|
+
return (boundary >= 4 ? name.slice(0, boundary) : name.slice(0, 16)).replace(/-+$/g, "") || null;
|
|
9
|
+
}
|
|
10
|
+
/** Two bounded tool-free attempts; cancellation never becomes an automatic fallback. */
|
|
11
|
+
export async function generateSessionName(prompt, hints, complete, signal, timeoutMs = 30_000) {
|
|
12
|
+
for (const maxTokens of [4_096, 32_768]) {
|
|
13
|
+
signal.throwIfAborted();
|
|
14
|
+
const deadline = new AbortController();
|
|
15
|
+
const attempt = AbortSignal.any([signal, deadline.signal]);
|
|
16
|
+
const timer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
17
|
+
timer.unref?.();
|
|
18
|
+
let aborted;
|
|
19
|
+
try {
|
|
20
|
+
const cancelled = new Promise((_, reject) => {
|
|
21
|
+
aborted = () => reject(attempt.reason);
|
|
22
|
+
attempt.addEventListener("abort", aborted, { once: true });
|
|
23
|
+
});
|
|
24
|
+
const response = await Promise.race([complete({
|
|
25
|
+
prompt: `Name this coding task with a lowercase hyphenated title of at most 16 characters. Reply only with the title.\nRepository hints: ${hints.slice(0, 300)}\n<task>\n${prompt.slice(0, 500)}\n</task>`,
|
|
26
|
+
maxTokens, signal: attempt,
|
|
27
|
+
}), cancelled]);
|
|
28
|
+
signal.throwIfAborted();
|
|
29
|
+
const name = response === null ? null : normalize(response);
|
|
30
|
+
if (name !== null)
|
|
31
|
+
return name;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
signal.throwIfAborted();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
if (aborted)
|
|
39
|
+
attempt.removeEventListener("abort", aborted);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
signal.throwIfAborted();
|
|
43
|
+
return normalize((prompt.toLowerCase().match(/[a-z0-9]+/g) ?? []).slice(0, 3).join("-")) ?? "new-task";
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SessionNamingController } from "./controller.js";
|
|
@@ -5,5 +5,5 @@ export { resolveProductPaths } from "./paths.js";
|
|
|
5
5
|
export type { ProductPaths } from "./paths.js";
|
|
6
6
|
export { parseSessionSelection, sessionSelectionArguments } from "./session-selection.js";
|
|
7
7
|
export type { SessionSelection } from "./session-selection.js";
|
|
8
|
-
export { PRIMARY_SESSION_AGENT_ID, activateSessionRepositoryContext, clearSessionRepositoryContext, listSessionRepositoryWorktrees, readSessionRepositoryContext, registerSessionRepositoryRuntime, releaseSessionRepositoryRuntime, setSessionRepositoryContext, } from "./session-repository-context.js";
|
|
9
|
-
export type { SessionRepositoryContext, SessionRepositoryContextOptions, SessionRepositoryIdentity, SessionWorktreeInventoryEntry, SessionWorktreeStatus, } from "./session-repository-context.js";
|
|
8
|
+
export { PRIMARY_SESSION_AGENT_ID, activateSessionRepositoryContext, clearSessionRepositoryContext, listSessionRepositoryWorktrees, readSessionRepositoryContext, registerSessionRepositoryRuntime, releaseSessionRepositoryRuntime, setSessionRepositoryContext, withSessionRepositoryObservation, } from "./session-repository-context.js";
|
|
9
|
+
export type { SessionRepositoryContext, SessionRepositoryContextOptions, SessionRepositoryIdentity, SessionRepositoryObservation, SessionWorktreeInventoryEntry, SessionWorktreeStatus, } from "./session-repository-context.js";
|
|
@@ -2,4 +2,4 @@ export { assertLaunchProfileId } from "./model.js";
|
|
|
2
2
|
export { PID_LIVENESS_IDENTITY, inspectLiveProcess } from "./process-liveness.js";
|
|
3
3
|
export { resolveProductPaths } from "./paths.js";
|
|
4
4
|
export { parseSessionSelection, sessionSelectionArguments } from "./session-selection.js";
|
|
5
|
-
export { PRIMARY_SESSION_AGENT_ID, activateSessionRepositoryContext, clearSessionRepositoryContext, listSessionRepositoryWorktrees, readSessionRepositoryContext, registerSessionRepositoryRuntime, releaseSessionRepositoryRuntime, setSessionRepositoryContext, } from "./session-repository-context.js";
|
|
5
|
+
export { PRIMARY_SESSION_AGENT_ID, activateSessionRepositoryContext, clearSessionRepositoryContext, listSessionRepositoryWorktrees, readSessionRepositoryContext, registerSessionRepositoryRuntime, releaseSessionRepositoryRuntime, setSessionRepositoryContext, withSessionRepositoryObservation, } from "./session-repository-context.js";
|
|
@@ -37,5 +37,17 @@ export declare function setSessionRepositoryContext(identity: SessionRepositoryI
|
|
|
37
37
|
export declare function clearSessionRepositoryContext(identity: SessionRepositoryIdentity, options?: SessionRepositoryContextOptions): Promise<void>;
|
|
38
38
|
/** Read and revalidate a persisted association; malformed or stale state fails closed to absence. */
|
|
39
39
|
export declare function readSessionRepositoryContext(identity: SessionRepositoryIdentity, options?: SessionRepositoryContextOptions): Promise<SessionRepositoryContext | null>;
|
|
40
|
+
/** A verified naming context. Revision changes on explicit association transitions, not heartbeats. */
|
|
41
|
+
export interface SessionRepositoryObservation {
|
|
42
|
+
readonly cwd: string;
|
|
43
|
+
readonly branch: string | null;
|
|
44
|
+
readonly revision: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Observe and synchronously consume a context under the existing mutation gate. Missing records
|
|
48
|
+
* mean unlinked; malformed, inaccessible, or invalid associations throw instead of inventing exits.
|
|
49
|
+
* The callback must not perform asynchronous work; provider I/O never holds this gate.
|
|
50
|
+
*/
|
|
51
|
+
export declare function withSessionRepositoryObservation<T>(identity: SessionRepositoryIdentity, consume: (context: SessionRepositoryObservation) => T, options?: SessionRepositoryContextOptions): Promise<T>;
|
|
40
52
|
/** List same-repository worktrees without acquiring, releasing, or repairing a claim. */
|
|
41
53
|
export declare function listSessionRepositoryWorktrees(identity: SessionRepositoryIdentity, options?: SessionRepositoryContextOptions): Promise<readonly SessionWorktreeInventoryEntry[]>;
|
|
@@ -86,9 +86,12 @@ export async function setSessionRepositoryContext(identity, worktreePath, option
|
|
|
86
86
|
if (status === "unverifiable")
|
|
87
87
|
throw contextError("worktree ownership cannot be verified", "worktree-owner-unverifiable");
|
|
88
88
|
const prior = await readContextRecord(header, options);
|
|
89
|
+
const unchanged = prior !== null && samePath(prior.worktreeGitDir, selected.gitDir)
|
|
90
|
+
&& await validateContextRecord(header, prior, options.signal).then(() => true, () => false);
|
|
91
|
+
const revision = unchanged ? prior.revision ?? randomUUID() : randomUUID();
|
|
89
92
|
await writeClaim(header, selected, owner, processIdentityOf(runtime), options);
|
|
90
93
|
try {
|
|
91
|
-
await writeContextRecord(header, startup, selected, options);
|
|
94
|
+
await writeContextRecord(header, startup, selected, revision, options);
|
|
92
95
|
}
|
|
93
96
|
catch (error) {
|
|
94
97
|
if (status !== "current")
|
|
@@ -106,7 +109,7 @@ export async function setSessionRepositoryContext(identity, worktreePath, option
|
|
|
106
109
|
export async function clearSessionRepositoryContext(identity, options = {}) {
|
|
107
110
|
const header = await readSessionHeader(identity);
|
|
108
111
|
if (options.runtimeId === undefined) {
|
|
109
|
-
await rm(contextRecordPath(header, options), { force: true });
|
|
112
|
+
await withClaimLock(options, async () => { await rm(contextRecordPath(header, options), { force: true }); });
|
|
110
113
|
return;
|
|
111
114
|
}
|
|
112
115
|
const owner = { runtimeId: requireRuntimeId(options), agentId: requireAgentId(options) };
|
|
@@ -131,6 +134,57 @@ export async function readSessionRepositoryContext(identity, options = {}) {
|
|
|
131
134
|
const selected = await validateContextRecord(header, record, options.signal).catch(() => null);
|
|
132
135
|
return selected === null ? null : { cwd: selected.root, branch: requireBranch(selected) };
|
|
133
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Observe and synchronously consume a context under the existing mutation gate. Missing records
|
|
139
|
+
* mean unlinked; malformed, inaccessible, or invalid associations throw instead of inventing exits.
|
|
140
|
+
* The callback must not perform asynchronous work; provider I/O never holds this gate.
|
|
141
|
+
*/
|
|
142
|
+
export async function withSessionRepositoryObservation(identity, consume, options = {}) {
|
|
143
|
+
const header = await readSessionHeader(identity);
|
|
144
|
+
return await withClaimLock(options, async () => {
|
|
145
|
+
let record = null;
|
|
146
|
+
try {
|
|
147
|
+
const path = contextRecordPath(header, options);
|
|
148
|
+
const metadata = await lstat(path);
|
|
149
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_RECORD_BYTES)
|
|
150
|
+
throw new Error("invalid repository context");
|
|
151
|
+
const value = JSON.parse(await readFile(path, { encoding: "utf8", signal: options.signal }));
|
|
152
|
+
if (!isContextRecord(value))
|
|
153
|
+
throw new Error("invalid repository context");
|
|
154
|
+
record = value;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
let context;
|
|
161
|
+
if (record !== null) {
|
|
162
|
+
const selected = await validateContextRecord(header, record, options.signal);
|
|
163
|
+
const revision = record.revision ?? randomUUID();
|
|
164
|
+
if (record.revision === undefined)
|
|
165
|
+
await atomicWrite(contextRecordPath(header, options), { ...record, revision });
|
|
166
|
+
context = { cwd: selected.root, branch: selected.branch, revision };
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
let cwd = await realpath(header.cwd);
|
|
170
|
+
let branch;
|
|
171
|
+
try {
|
|
172
|
+
const selected = await readGitWorktreeIdentity(cwd, false, options.signal);
|
|
173
|
+
cwd = selected.root;
|
|
174
|
+
const result = await executeGit(["branch", "--show-current"], cwd, 4_096, options.signal);
|
|
175
|
+
branch = result.stdout.trim() || null;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (!(error instanceof Error && "stderr" in error && String(error.stderr).includes("not a git repository")))
|
|
179
|
+
throw error;
|
|
180
|
+
branch = null;
|
|
181
|
+
}
|
|
182
|
+
context = { cwd, branch, revision: "unlinked" };
|
|
183
|
+
}
|
|
184
|
+
options.signal?.throwIfAborted();
|
|
185
|
+
return consume(context);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
134
188
|
/** List same-repository worktrees without acquiring, releasing, or repairing a claim. */
|
|
135
189
|
export async function listSessionRepositoryWorktrees(identity, options = {}) {
|
|
136
190
|
const header = await readSessionHeader(identity);
|
|
@@ -223,9 +277,10 @@ async function writeRuntimeRecord(header, runtimeId, agentId, runtimeIdentity, o
|
|
|
223
277
|
};
|
|
224
278
|
await atomicWrite(runtimeRecordPath({ runtimeId, agentId }, options), record);
|
|
225
279
|
}
|
|
226
|
-
async function writeContextRecord(header, startup, selected, options) {
|
|
280
|
+
async function writeContextRecord(header, startup, selected, revision, options) {
|
|
227
281
|
const record = {
|
|
228
282
|
schema: CONTEXT_SCHEMA,
|
|
283
|
+
revision,
|
|
229
284
|
sessionId: header.id,
|
|
230
285
|
sessionFile: header.file,
|
|
231
286
|
startupRoot: startup.root,
|
|
@@ -554,6 +609,7 @@ function validPath(value) {
|
|
|
554
609
|
}
|
|
555
610
|
function isContextRecord(value) {
|
|
556
611
|
return isRecord(value) && value.schema === CONTEXT_SCHEMA && validSessionId(value.sessionId)
|
|
612
|
+
&& (value.revision === undefined || validSessionId(value.revision))
|
|
557
613
|
&& validPath(value.sessionFile) && validPath(value.startupRoot) && validPath(value.startupCommonDir)
|
|
558
614
|
&& validPath(value.worktreeRoot) && validPath(value.worktreeCommonDir) && validPath(value.worktreeGitDir)
|
|
559
615
|
&& typeof value.updatedAt === "string";
|
|
@@ -142,6 +142,7 @@ export function createPiShellFooter(view, cwd, profile = "pi", showLevel = () =>
|
|
|
142
142
|
const footer = new SessionFooter(() => view, cwd, profile, showLevel);
|
|
143
143
|
return {
|
|
144
144
|
render: width => footer.render(width),
|
|
145
|
+
setNameReveal: frame => footer.setNameReveal(frame),
|
|
145
146
|
invalidate: () => footer.invalidate(),
|
|
146
147
|
update(next) { view = next; },
|
|
147
148
|
dispose: () => footer.dispose(),
|
|
@@ -123,20 +123,17 @@ function withTranscriptImages(component, block, assets, showImages, imageWidthCe
|
|
|
123
123
|
if (component instanceof ToolExecutionComponent)
|
|
124
124
|
return component;
|
|
125
125
|
const references = block.imageReferences ?? [];
|
|
126
|
-
if (references.length === 0)
|
|
126
|
+
if (!showImages || references.length === 0)
|
|
127
127
|
return component;
|
|
128
128
|
const container = new Container();
|
|
129
129
|
container.addChild(component);
|
|
130
130
|
const imageProtocolUnavailable = getCapabilities().images === null;
|
|
131
131
|
for (const reference of references) {
|
|
132
|
-
if (
|
|
132
|
+
if (imageProtocolUnavailable)
|
|
133
133
|
continue;
|
|
134
134
|
container.addChild(new Spacer(1));
|
|
135
135
|
const asset = assets?.resolve(reference.assetId) ?? null;
|
|
136
|
-
if (
|
|
137
|
-
container.addChild(new Text(piTheme().fg("muted", `[Image hidden: ${reference.mimeType}, ${reference.byteLength} bytes]`), 1, 0));
|
|
138
|
-
}
|
|
139
|
-
else if (asset === null) {
|
|
136
|
+
if (asset === null) {
|
|
140
137
|
container.addChild(new Text(piTheme().fg("warning", `[Image unavailable: ${reference.mimeType}]`), 1, 0));
|
|
141
138
|
}
|
|
142
139
|
else {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Component } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { OwnedUiSessionViewModel } from "../../../../../contracts/owned-ui/index.js";
|
|
3
3
|
export declare class SessionFooter implements Component {
|
|
4
|
+
#private;
|
|
4
5
|
private readonly getView;
|
|
5
6
|
private readonly cwd;
|
|
6
7
|
private readonly profile;
|
|
@@ -8,5 +9,9 @@ export declare class SessionFooter implements Component {
|
|
|
8
9
|
constructor(getView: () => OwnedUiSessionViewModel, cwd: string, profile?: "pi" | "a1", show?: () => boolean);
|
|
9
10
|
invalidate(): void;
|
|
10
11
|
dispose(): void;
|
|
12
|
+
setNameReveal(frame: {
|
|
13
|
+
readonly from: string;
|
|
14
|
+
readonly elapsed: number;
|
|
15
|
+
} | null): boolean;
|
|
11
16
|
render(width: number): string[];
|
|
12
17
|
}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provenance: @earendil-works/pi-coding-agent 1.5.0 (MIT), commit f9a26365568127461c47a07a37dcb42c140a3ac5,
|
|
3
3
|
* packages/coding-agent/src/modes/interactive/components/footer.ts.
|
|
4
|
-
* Modifications: Owned UI/routing; A1 level color, linked PRs, dim ellipsis
|
|
5
|
-
* unchanged.
|
|
4
|
+
* Modifications: Owned UI/routing; A1 level color, linked PRs, dim ellipsis, model priority, and
|
|
5
|
+
* secondary-accent name reveals after stats; Pi unchanged.
|
|
6
6
|
* Deviations: owned-status-level-color, owned-pull-request-badge, owned-footer-ellipsis,
|
|
7
|
-
* owned-footer-model-priority.
|
|
7
|
+
* owned-footer-model-priority, owned-session-name-reveal.
|
|
8
8
|
*/
|
|
9
9
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
10
10
|
import { hyperlink, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
11
|
import { piTheme } from "../../theme.js";
|
|
12
|
+
import { sessionNameFrame } from "../../../../../ui/components/session-name-frame.js";
|
|
12
13
|
export class SessionFooter {
|
|
13
14
|
getView;
|
|
14
15
|
cwd;
|
|
15
16
|
profile;
|
|
16
17
|
show;
|
|
18
|
+
#reveal = null;
|
|
19
|
+
#nameBudget = Number.MAX_SAFE_INTEGER;
|
|
17
20
|
constructor(getView, cwd, profile = "pi", show = () => true) {
|
|
18
21
|
this.getView = getView;
|
|
19
22
|
this.cwd = cwd;
|
|
@@ -21,7 +24,33 @@ export class SessionFooter {
|
|
|
21
24
|
this.show = show;
|
|
22
25
|
}
|
|
23
26
|
invalidate() { }
|
|
24
|
-
dispose() { }
|
|
27
|
+
dispose() { this.#reveal = null; }
|
|
28
|
+
setNameReveal(frame) {
|
|
29
|
+
this.#reveal = this.profile === "a1" ? frame : null;
|
|
30
|
+
if (this.#reveal && this.#nameFrame(this.getView().status.footer?.sessionName ?? "", this.#nameBudget).done)
|
|
31
|
+
this.#reveal = null;
|
|
32
|
+
return this.#reveal !== null;
|
|
33
|
+
}
|
|
34
|
+
#nameFrame(name, width) {
|
|
35
|
+
const theme = piTheme();
|
|
36
|
+
const secondary = rgb(theme.getFgAnsi("mdHeading"));
|
|
37
|
+
const settled = rgb(theme.getFgAnsi("dim"));
|
|
38
|
+
if (!this.#reveal)
|
|
39
|
+
return { text: theme.fg("dim", truncateToWidth(name, width, "…")), done: true };
|
|
40
|
+
return sessionNameFrame({
|
|
41
|
+
...this.#reveal, to: name, width,
|
|
42
|
+
truecolor: theme.getColorMode() === "truecolor" && secondary !== null && settled !== null,
|
|
43
|
+
measure: visibleWidth, clip: (text, columns) => truncateToWidth(text, columns, "…"),
|
|
44
|
+
dim: text => theme.fg("dim", text),
|
|
45
|
+
paint: (text, brightness, progress) => {
|
|
46
|
+
const channels = secondary.map((value, index) => {
|
|
47
|
+
const bright = value + (235 - value) * brightness;
|
|
48
|
+
return Math.round(bright + (settled[index] - bright) * progress);
|
|
49
|
+
});
|
|
50
|
+
return `\u001b[38;2;${channels.join(";")}m${text}\u001b[39m`;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
25
54
|
render(width) {
|
|
26
55
|
const view = this.getView();
|
|
27
56
|
const usage = view.status.usage;
|
|
@@ -45,13 +74,12 @@ export class SessionFooter {
|
|
|
45
74
|
const pullRequest = this.profile === "a1" ? view.status.footer?.pullRequest : null;
|
|
46
75
|
let pathRow;
|
|
47
76
|
if (pullRequest == null) {
|
|
48
|
-
pathRow = theme.fg("dim", sessionName ? `${pwd} • ${sessionName}` : pwd);
|
|
77
|
+
pathRow = theme.fg("dim", this.profile === "pi" && sessionName ? `${pwd} • ${sessionName}` : pwd);
|
|
49
78
|
}
|
|
50
79
|
else {
|
|
51
80
|
const badge = theme.fg("dim", " ")
|
|
52
81
|
+ hyperlink(theme.fg("mdLink", `#${pullRequest.number}`), pullRequest.url);
|
|
53
|
-
const
|
|
54
|
-
const full = theme.fg("dim", pwd) + badge + sessionSuffix;
|
|
82
|
+
const full = theme.fg("dim", pwd) + badge;
|
|
55
83
|
if (visibleWidth(full) <= width)
|
|
56
84
|
pathRow = full;
|
|
57
85
|
else if (visibleWidth(badge) <= width) {
|
|
@@ -111,6 +139,11 @@ export class SessionFooter {
|
|
|
111
139
|
leftWidth = visibleWidth(left);
|
|
112
140
|
}
|
|
113
141
|
const rightWidth = visibleWidth(right);
|
|
142
|
+
this.#nameBudget = Math.max(0, width - leftWidth - rightWidth - 2 - 3);
|
|
143
|
+
if (this.profile === "a1" && sessionName && this.#nameBudget > 0) {
|
|
144
|
+
left += theme.fg("dim", " • ") + this.#nameFrame(sessionName, this.#nameBudget).text;
|
|
145
|
+
leftWidth = visibleWidth(left);
|
|
146
|
+
}
|
|
114
147
|
let line;
|
|
115
148
|
if (this.profile === "a1" && leftWidth === 0) {
|
|
116
149
|
line = " ".repeat(Math.max(0, width - rightWidth)) + right;
|
|
@@ -139,6 +172,10 @@ export class SessionFooter {
|
|
|
139
172
|
return lines;
|
|
140
173
|
}
|
|
141
174
|
}
|
|
175
|
+
function rgb(ansi) {
|
|
176
|
+
const match = /\u001b\[38;2;(\d+);(\d+);(\d+)m/u.exec(ansi);
|
|
177
|
+
return match ? match.slice(1).map(Number) : null;
|
|
178
|
+
}
|
|
142
179
|
function sanitize(text) { return text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim(); }
|
|
143
180
|
function formatTokens(count) {
|
|
144
181
|
if (count < 1000)
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* Modifications: Retain pinned shell/public renderers. Own image conversion; replace private APIs with
|
|
5
5
|
* public/local equivalents. Use bare-A1 secondary-accent read ranges and concise collapse shortcut
|
|
6
6
|
* hints. Restore the enclosing default-shell background after nested renderer resets. Omit
|
|
7
|
-
* non-actionable image fallback rows when the terminal has no image protocol. See
|
|
7
|
+
* non-actionable image fallback rows when images are hidden or the terminal has no image protocol. See
|
|
8
8
|
* docs/architecture/tool-image-presentation.md.
|
|
9
9
|
* Deviations: current-tool-image-conversion-ownership, read-range-secondary-accent,
|
|
10
10
|
* concise-tool-expand-shortcut-hint, default-shell-background-reset-scoping,
|
|
11
|
-
* unsupported-image-fallback-omission.
|
|
11
|
+
* unsupported-image-fallback-omission, hidden-image-fallback-omission.
|
|
12
12
|
*/
|
|
13
13
|
import { stripVTControlCharacters } from "node:util";
|
|
14
|
-
import { Box, Container,
|
|
14
|
+
import { Box, Container, MouseRegion, Spacer, Text } from "@earendil-works/pi-tui";
|
|
15
15
|
import { createReadToolDefinition, createBashToolDefinition, createEditToolDefinition, createWriteToolDefinition, createGrepToolDefinition, createFindToolDefinition, createLsToolDefinition, keyHint, keyText, } from "../../../startup-public.js";
|
|
16
16
|
import { renderPiModalShortcutHints } from "../../theme.js";
|
|
17
17
|
import { isPiAccentProjectionEnabled, piTheme } from "../theme/theme.js";
|
|
@@ -392,11 +392,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
392
392
|
getTextOutput() {
|
|
393
393
|
if (!this.result)
|
|
394
394
|
return "";
|
|
395
|
-
|
|
396
|
-
if (this.showImages)
|
|
397
|
-
return text;
|
|
398
|
-
const indicators = this.result.content.filter(part => part.type === "image").map(part => imageFallback(part.mimeType ?? "image/unknown", part.data && part.mimeType ? getImageDimensions(part.data, part.mimeType) ?? undefined : undefined)).join("\n");
|
|
399
|
-
return text && indicators ? `${text}\n${indicators}` : text || indicators;
|
|
395
|
+
return this.result.content.filter(part => part.type === "text").map(part => stripVTControlCharacters(part.text ?? "").replace(/[\u0000-\u0008\u000b-\u001f\ufff9-\ufffb]/gu, "")).join("\n");
|
|
400
396
|
}
|
|
401
397
|
formatToolExecution() {
|
|
402
398
|
const theme = piTheme();
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type OwnedUiCommand, type OwnedUiExtensionPort, type OwnedUiPromptSuggestionGeneratorPort, type OwnedUiPromptSuggestionReasoning, type OwnedUiPromptSuggestionRequest, type OwnedUiPromptSuggestionResult, type OwnedUiEvent, type OwnedUiSessionBackend, type OwnedUiSessionCatalogPort, type OwnedUiSessionIdentityPort, type OwnedUiSessionPort, type OwnedUiSessionSettingsPort, type OwnedUiSessionViewModel, type OwnedUiSnapshot, type OwnedUiWorkflowPort } from "../../../contracts/owned-ui/index.js";
|
|
2
|
-
import type { PiWorkflowHost,
|
|
2
|
+
import type { PiWorkflowHost, PiWorkflowRequest, PiWorkflowResult } from "./workflows.js";
|
|
3
3
|
import type { PiSettingsBridge } from "./settings-bridge.js";
|
|
4
4
|
import type { PiProjectTrustPreflightPrompt } from "./project-trust-preflight.js";
|
|
5
5
|
import type { PiSessionForkPrompt, PiSessionSelection } from "./session-selection.js";
|
|
6
6
|
import { type AdapterCommandResult } from "./command-dispatch.js";
|
|
7
|
+
import { PiSessionNaming } from "./session-naming.js";
|
|
7
8
|
import { type PiEnginePackageUpdateProbe, type PiEngineRuntimeFactory, type PiRepositoryContextReader } from "./session-runtime.js";
|
|
8
9
|
import { type PiEngineHostSessionPorts } from "./host-ports.js";
|
|
9
10
|
import { type PiEngineAnnouncements, type PiSessionPresentationSource } from "./ports.js";
|
|
@@ -60,7 +61,7 @@ export declare class PiEngineAdapter implements OwnedUiSessionBackend, OwnedUiPr
|
|
|
60
61
|
get announcements(): PiEngineAnnouncements;
|
|
61
62
|
/** The Pi objects behind pinned selectors and renderers, for the session presenters owner only. */
|
|
62
63
|
presentationSource(): PiSessionPresentationSource;
|
|
63
|
-
|
|
64
|
+
get naming(): PiSessionNaming;
|
|
64
65
|
suggestionReasoningPolicy(): OwnedUiPromptSuggestionReasoning;
|
|
65
66
|
generate(request: OwnedUiPromptSuggestionRequest): Promise<OwnedUiPromptSuggestionResult>;
|
|
66
67
|
start(): Promise<OwnedUiSessionViewModel>;
|