pi-sessions 0.12.1 → 0.13.1
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 +6 -3
- package/extensions/pi-sessions.ts +20 -6
- package/extensions/session-ask/install.ts +24 -10
- package/extensions/session-auto-title/generate.ts +17 -14
- package/extensions/session-auto-title/install.ts +6 -29
- package/extensions/session-auto-title/retitle.ts +6 -19
- package/extensions/session-auto-title/wizard.ts +97 -31
- package/extensions/session-handoff/board-loading.ts +93 -0
- package/extensions/session-handoff/board.ts +239 -127
- package/extensions/session-handoff/install.ts +5 -5
- package/extensions/session-handoff/picker.ts +167 -19
- package/extensions/session-handoff/review.ts +56 -7
- package/extensions/session-handoff/ui.ts +32 -7
- package/extensions/session-hooks/install.ts +7 -20
- package/extensions/session-index/install.ts +8 -86
- package/extensions/session-index/loader.ts +80 -0
- package/extensions/session-index/panel.ts +103 -0
- package/extensions/session-messaging/broker/spawn.ts +35 -12
- package/extensions/session-messaging/pi/reachable-tool.ts +6 -1
- package/extensions/session-messaging/pi/tools.ts +79 -12
- package/extensions/session-search/extract.ts +120 -91
- package/extensions/session-search/hooks.ts +25 -203
- package/extensions/session-search/reindex.ts +13 -8
- package/extensions/shared/legend.ts +116 -0
- package/extensions/shared/session-index/scoring.ts +0 -1
- package/extensions/shared/session-index/search.ts +76 -49
- package/extensions/shared/settings.ts +22 -0
- package/extensions/subagents/context-limit.ts +58 -0
- package/extensions/subagents/install.ts +110 -30
- package/extensions/subagents/reconcile.ts +58 -19
- package/extensions/subagents/report.ts +0 -2
- package/extensions/subagents/settle.ts +31 -0
- package/extensions/subagents/should-message.ts +59 -0
- package/package.json +13 -13
- package/extensions/session-handoff/refs.ts +0 -145
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
## Install
|
|
36
36
|
|
|
37
|
-
Requires Pi `0.
|
|
37
|
+
Requires Pi `0.87.1` or newer and Node `>=24 <26`.
|
|
38
38
|
|
|
39
39
|
**From npm** (recommended):
|
|
40
40
|
|
|
@@ -157,18 +157,21 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
|
|
|
157
157
|
|
|
158
158
|
`deferred.copyToClipboard` (default `true`) controls whether deferred handoffs copy the resume command to the clipboard. When off, the resume command is only shown in the tool call.
|
|
159
159
|
|
|
160
|
-
Subagents require the handoff and messaging features. Limit recursive delegation depth with `sessions.subagents.maxDepth` (default `2`)
|
|
160
|
+
Subagents require the handoff and messaging features. Limit recursive delegation depth with `sessions.subagents.maxDepth` (default `2`), or cap the context a subagent runs in with `sessions.subagents.contextLimit`:
|
|
161
161
|
|
|
162
162
|
```json
|
|
163
163
|
{
|
|
164
164
|
"sessions": {
|
|
165
165
|
"subagents": {
|
|
166
|
-
"maxDepth": 2
|
|
166
|
+
"maxDepth": 2,
|
|
167
|
+
"contextLimit": 400000
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
```
|
|
171
172
|
|
|
173
|
+
`contextLimit` sets a max context limit before compaction is triggered. It works identically to setting a global value in pi itself, but only applies to subagents.
|
|
174
|
+
|
|
172
175
|
## Session Index
|
|
173
176
|
|
|
174
177
|
By default, `pi-sessions` will start indexing all conversations moving forward. If you want to backfill all prior conversations:
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { installSearch } from "./session-search/install.ts";
|
|
18
18
|
import type { SessionLifecycle } from "./shared/composition.ts";
|
|
19
19
|
import { createSessionModelRuntime, type ModelRuntimeProvider } from "./shared/model-runtime.ts";
|
|
20
|
-
import { loadSettings } from "./shared/settings.ts";
|
|
20
|
+
import { loadSettings, readCompactionSettings } from "./shared/settings.ts";
|
|
21
21
|
import { installSubagents } from "./subagents/install.ts";
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -55,7 +55,7 @@ export default function piSessions(pi: ExtensionAPI): void {
|
|
|
55
55
|
}
|
|
56
56
|
const subagents =
|
|
57
57
|
settings.features.subagents && messaging
|
|
58
|
-
? installSubagents(pi, { settings, messaging })
|
|
58
|
+
? installSubagents(pi, { settings, messaging, readCompactionSettings })
|
|
59
59
|
: undefined;
|
|
60
60
|
if (subagents) {
|
|
61
61
|
lifecycles.push(subagents);
|
|
@@ -63,11 +63,17 @@ export default function piSessions(pi: ExtensionAPI): void {
|
|
|
63
63
|
if (messaging) {
|
|
64
64
|
pi.registerTool(
|
|
65
65
|
createSessionSendMessageTool(subagents ?? messaging, {
|
|
66
|
-
wakeCapable:
|
|
66
|
+
role: subagents ? { kind: "wakeCapable" } : { kind: "plain" },
|
|
67
67
|
getCachedRelationTo: messaging.getCachedRelationTo,
|
|
68
|
+
...(subagents ? { getParentSessionId: () => subagents.getParentSessionId() } : {}),
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
pi.registerTool(
|
|
72
|
+
createSessionCancelTool(subagents ?? messaging, {
|
|
73
|
+
role: { kind: "plain" },
|
|
74
|
+
...(subagents ? { getParentSessionId: () => subagents.getParentSessionId() } : {}),
|
|
68
75
|
}),
|
|
69
76
|
);
|
|
70
|
-
pi.registerTool(createSessionCancelTool(subagents ?? messaging));
|
|
71
77
|
pi.registerTool(
|
|
72
78
|
createSessionReachableTool({
|
|
73
79
|
indexPath: index.path,
|
|
@@ -76,6 +82,7 @@ export default function piSessions(pi: ExtensionAPI): void {
|
|
|
76
82
|
...(subagents
|
|
77
83
|
? {
|
|
78
84
|
listSubagents: async (scope) => (await subagents.roster.resolve(scope)).entries,
|
|
85
|
+
getParentSessionId: () => subagents.getParentSessionId(),
|
|
79
86
|
}
|
|
80
87
|
: {}),
|
|
81
88
|
}),
|
|
@@ -104,7 +111,14 @@ export default function piSessions(pi: ExtensionAPI): void {
|
|
|
104
111
|
installSearch(pi, { settings, index });
|
|
105
112
|
}
|
|
106
113
|
if (settings.features.ask) {
|
|
107
|
-
installAsk(pi, {
|
|
114
|
+
installAsk(pi, {
|
|
115
|
+
settings,
|
|
116
|
+
index,
|
|
117
|
+
getModelRuntime,
|
|
118
|
+
...(subagents
|
|
119
|
+
? { shouldMessageSubagent: (sessionId) => subagents.shouldMessageSubagent(sessionId) }
|
|
120
|
+
: {}),
|
|
121
|
+
});
|
|
108
122
|
}
|
|
109
123
|
if (settings.features.autoTitle) {
|
|
110
124
|
lifecycles.push(
|
|
@@ -116,7 +130,7 @@ export default function piSessions(pi: ExtensionAPI): void {
|
|
|
116
130
|
);
|
|
117
131
|
}
|
|
118
132
|
if (settings.features.hooks) {
|
|
119
|
-
lifecycles.push(installHooks(pi, {
|
|
133
|
+
lifecycles.push(installHooks(pi, { index }));
|
|
120
134
|
}
|
|
121
135
|
// Messaging leads every session_start so the broker connection is registered before any
|
|
122
136
|
// other feature hook runs. A side effect is that messaging's first relation snapshot can
|
|
@@ -29,6 +29,7 @@ export function installAsk(
|
|
|
29
29
|
settings: SessionSettings;
|
|
30
30
|
index: IndexHandle;
|
|
31
31
|
getModelRuntime: ModelRuntimeProvider;
|
|
32
|
+
shouldMessageSubagent?: (sessionId: string) => Promise<boolean>;
|
|
32
33
|
},
|
|
33
34
|
): void {
|
|
34
35
|
const { settings } = deps;
|
|
@@ -37,15 +38,22 @@ export function installAsk(
|
|
|
37
38
|
pi.registerTool({
|
|
38
39
|
name: "session_ask",
|
|
39
40
|
label: "Session Ask",
|
|
40
|
-
description: "Interrogate a Pi session",
|
|
41
|
+
description: "Interrogate a Pi session transcript",
|
|
41
42
|
promptSnippet: "Recall information, decisions, or reasoning from a Pi session by id",
|
|
42
|
-
promptGuidelines: [
|
|
43
|
+
promptGuidelines: [
|
|
44
|
+
"Use session_ask with focused questions rather than broad recap requests.",
|
|
45
|
+
...(deps.shouldMessageSubagent
|
|
46
|
+
? [
|
|
47
|
+
"Use session_send_message for your unfinished subagents; session_ask can read completed subagent transcripts.",
|
|
48
|
+
]
|
|
49
|
+
: []),
|
|
50
|
+
],
|
|
43
51
|
parameters: Type.Object({
|
|
44
52
|
session: Type.String({
|
|
45
53
|
description: "Bare UUID for the session",
|
|
46
54
|
}),
|
|
47
55
|
question: Type.String({
|
|
48
|
-
description: "What to
|
|
56
|
+
description: "What to find in that session's transcript",
|
|
49
57
|
}),
|
|
50
58
|
}),
|
|
51
59
|
async execute(_toolCallId, params: SessionAskToolParams, signal, onUpdate, ctx) {
|
|
@@ -59,6 +67,19 @@ export function installAsk(
|
|
|
59
67
|
throw new Error("session_ask requires a question.");
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
if (!isExactSessionId(sessionId)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"session_ask requires an exact session UUID. Use autocomplete or session_search to find it.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Unfinished children may not have reached the index yet.
|
|
77
|
+
if (await deps.shouldMessageSubagent?.(sessionId)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"That session is your unfinished subagent. Use session_send_message to ask it directly.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
const resolvedTarget = resolveSessionAskTarget(sessionId, indexPath);
|
|
63
84
|
if (!resolvedTarget.resolved) {
|
|
64
85
|
throw new Error(resolvedTarget.error ?? "Unable to resolve session id.");
|
|
@@ -182,13 +203,6 @@ function resolveSessionAskTarget(
|
|
|
182
203
|
resolved?: SessionLineageRow | undefined;
|
|
183
204
|
error?: string | undefined;
|
|
184
205
|
} {
|
|
185
|
-
if (!isExactSessionId(sessionId)) {
|
|
186
|
-
return {
|
|
187
|
-
error:
|
|
188
|
-
"session_ask requires an exact session UUID. Use autocomplete or session_search to find it.",
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
206
|
try {
|
|
193
207
|
return withSessionIndex(indexPath, { mode: "read", required: true }, ({ db }) => {
|
|
194
208
|
const row = getSessionById(db, sessionId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Api, Model, TextContent, UserMessage } from "@earendil-works/pi-ai";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { AutoTitleContext } from "./context.ts";
|
|
5
5
|
import { type AutoTitleRun, startAutoTitleRun } from "./runs.ts";
|
|
6
6
|
import type { AutoTitleTrigger } from "./state.ts";
|
|
@@ -33,7 +33,7 @@ export type AutoTitleGenerationResult =
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
export async function generateAutoTitle(
|
|
36
|
-
|
|
36
|
+
modelRegistry: ModelRegistry,
|
|
37
37
|
model: Model<Api>,
|
|
38
38
|
context: AutoTitleContext,
|
|
39
39
|
trigger: AutoTitleTrigger,
|
|
@@ -78,18 +78,21 @@ export async function generateAutoTitle(
|
|
|
78
78
|
: undefined;
|
|
79
79
|
|
|
80
80
|
try {
|
|
81
|
-
const response = await
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
const response = await modelRegistry
|
|
82
|
+
.streamSimple(
|
|
83
|
+
model,
|
|
84
|
+
{
|
|
85
|
+
systemPrompt: resolvedSystemPrompt,
|
|
86
|
+
messages: [message],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
maxTokens: generation.tokenBudget,
|
|
90
|
+
...(thinkingLevel && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
|
|
91
|
+
signal: abortController.signal,
|
|
92
|
+
cacheRetention: "none",
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
.result();
|
|
93
96
|
run?.recordResponse(response);
|
|
94
97
|
|
|
95
98
|
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
@@ -3,7 +3,6 @@ import type {
|
|
|
3
3
|
ExtensionAPI,
|
|
4
4
|
ExtensionCommandContext,
|
|
5
5
|
ExtensionContext,
|
|
6
|
-
ModelRuntime,
|
|
7
6
|
TurnEndEvent,
|
|
8
7
|
} from "@earendil-works/pi-coding-agent";
|
|
9
8
|
import type { SessionLifecycle } from "../shared/composition.ts";
|
|
@@ -82,7 +81,6 @@ export function installAutoTitle(
|
|
|
82
81
|
},
|
|
83
82
|
},
|
|
84
83
|
ctx,
|
|
85
|
-
modelRuntime,
|
|
86
84
|
resolution?.model,
|
|
87
85
|
invocation,
|
|
88
86
|
buildGeneration(resolution),
|
|
@@ -105,7 +103,6 @@ export function installAutoTitle(
|
|
|
105
103
|
pi,
|
|
106
104
|
controller,
|
|
107
105
|
ctx,
|
|
108
|
-
modelRuntime,
|
|
109
106
|
model: resolution?.model,
|
|
110
107
|
isManual: false,
|
|
111
108
|
existingPlan: result.plan,
|
|
@@ -147,7 +144,6 @@ async function handleTitleInvocation(
|
|
|
147
144
|
pi: ExtensionAPI,
|
|
148
145
|
state: TitleRunState,
|
|
149
146
|
ctx: ExtensionCommandContext,
|
|
150
|
-
modelRuntime: ModelRuntime,
|
|
151
147
|
model: Model<Api> | undefined,
|
|
152
148
|
invocation: RetitleCommandInvocation,
|
|
153
149
|
generation: AutoTitleGeneration,
|
|
@@ -156,7 +152,6 @@ async function handleTitleInvocation(
|
|
|
156
152
|
pi,
|
|
157
153
|
controller: state.controller,
|
|
158
154
|
ctx,
|
|
159
|
-
modelRuntime,
|
|
160
155
|
model,
|
|
161
156
|
isManual: true,
|
|
162
157
|
generation,
|
|
@@ -178,15 +173,7 @@ async function handleTitleInvocation(
|
|
|
178
173
|
return retitleCurrentSession();
|
|
179
174
|
}
|
|
180
175
|
|
|
181
|
-
return showRetitleWizard(
|
|
182
|
-
pi,
|
|
183
|
-
state.controller,
|
|
184
|
-
ctx,
|
|
185
|
-
modelRuntime,
|
|
186
|
-
model,
|
|
187
|
-
state.getSessionEpoch,
|
|
188
|
-
generation,
|
|
189
|
-
);
|
|
176
|
+
return showRetitleWizard(pi, state.controller, ctx, model, state.getSessionEpoch, generation);
|
|
190
177
|
}
|
|
191
178
|
|
|
192
179
|
if (invocation.scope === "this") {
|
|
@@ -194,21 +181,12 @@ async function handleTitleInvocation(
|
|
|
194
181
|
}
|
|
195
182
|
|
|
196
183
|
if (isTuiMode(ctx) && !invocation.force) {
|
|
197
|
-
return showRetitleWizard(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
modelRuntime,
|
|
202
|
-
model,
|
|
203
|
-
state.getSessionEpoch,
|
|
204
|
-
generation,
|
|
205
|
-
{
|
|
206
|
-
initialInvocation: {
|
|
207
|
-
scope: invocation.scope,
|
|
208
|
-
mode: invocation.mode ?? "backfill",
|
|
209
|
-
},
|
|
184
|
+
return showRetitleWizard(pi, state.controller, ctx, model, state.getSessionEpoch, generation, {
|
|
185
|
+
initialInvocation: {
|
|
186
|
+
scope: invocation.scope,
|
|
187
|
+
mode: invocation.mode ?? "backfill",
|
|
210
188
|
},
|
|
211
|
-
);
|
|
189
|
+
});
|
|
212
190
|
}
|
|
213
191
|
|
|
214
192
|
const scan = await buildRetitleScopeScan(ctx, invocation.scope);
|
|
@@ -218,7 +196,6 @@ async function handleTitleInvocation(
|
|
|
218
196
|
pi,
|
|
219
197
|
state.controller,
|
|
220
198
|
ctx,
|
|
221
|
-
modelRuntime,
|
|
222
199
|
model,
|
|
223
200
|
scan,
|
|
224
201
|
mode,
|
|
@@ -3,7 +3,6 @@ import type {
|
|
|
3
3
|
ExtensionAPI,
|
|
4
4
|
ExtensionCommandContext,
|
|
5
5
|
ExtensionContext,
|
|
6
|
-
ModelRuntime,
|
|
7
6
|
SessionInfo,
|
|
8
7
|
} from "@earendil-works/pi-coding-agent";
|
|
9
8
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
@@ -55,7 +54,6 @@ export async function runBulkRetitle(
|
|
|
55
54
|
pi: ExtensionAPI,
|
|
56
55
|
controller: SessionAutoTitleController,
|
|
57
56
|
ctx: ExtensionCommandContext,
|
|
58
|
-
modelRuntime: ModelRuntime,
|
|
59
57
|
model: Model<Api> | undefined,
|
|
60
58
|
scan: RetitleScopeScan,
|
|
61
59
|
mode: RetitleMode,
|
|
@@ -85,7 +83,6 @@ export async function runBulkRetitle(
|
|
|
85
83
|
pi,
|
|
86
84
|
controller,
|
|
87
85
|
ctx,
|
|
88
|
-
modelRuntime,
|
|
89
86
|
model,
|
|
90
87
|
isManual: true,
|
|
91
88
|
generation,
|
|
@@ -96,7 +93,7 @@ export async function runBulkRetitle(
|
|
|
96
93
|
continue;
|
|
97
94
|
}
|
|
98
95
|
|
|
99
|
-
const outcome = await retitleStoredSession(
|
|
96
|
+
const outcome = await retitleStoredSession(ctx.modelRegistry, model, session.path, generation);
|
|
100
97
|
result[outcome] += 1;
|
|
101
98
|
}
|
|
102
99
|
|
|
@@ -166,7 +163,6 @@ export interface RetitlePlanOptions {
|
|
|
166
163
|
pi: ExtensionAPI;
|
|
167
164
|
controller: SessionAutoTitleController;
|
|
168
165
|
ctx: ExtensionContext;
|
|
169
|
-
modelRuntime: ModelRuntime;
|
|
170
166
|
model: Model<Api> | undefined;
|
|
171
167
|
isManual: boolean;
|
|
172
168
|
generation: AutoTitleGeneration;
|
|
@@ -178,17 +174,8 @@ export interface RetitlePlanOptions {
|
|
|
178
174
|
export async function runRetitlePlan(
|
|
179
175
|
options: RetitlePlanOptions,
|
|
180
176
|
): Promise<AutoTitleGenerationResult> {
|
|
181
|
-
const {
|
|
182
|
-
|
|
183
|
-
controller,
|
|
184
|
-
ctx,
|
|
185
|
-
modelRuntime,
|
|
186
|
-
model,
|
|
187
|
-
isManual,
|
|
188
|
-
generation,
|
|
189
|
-
existingPlan,
|
|
190
|
-
getSessionEpoch,
|
|
191
|
-
} = options;
|
|
177
|
+
const { pi, controller, ctx, model, isManual, generation, existingPlan, getSessionEpoch } =
|
|
178
|
+
options;
|
|
192
179
|
const notifyOnSuccess = options.notifyOnSuccess ?? isManual;
|
|
193
180
|
|
|
194
181
|
const plan = existingPlan ?? controller.handleManualRetitle(ctx);
|
|
@@ -220,7 +207,7 @@ export async function runRetitlePlan(
|
|
|
220
207
|
},
|
|
221
208
|
);
|
|
222
209
|
const generatedTitle = await generateAutoTitle(
|
|
223
|
-
|
|
210
|
+
ctx.modelRegistry,
|
|
224
211
|
model,
|
|
225
212
|
titleContext,
|
|
226
213
|
plan.reason,
|
|
@@ -267,7 +254,7 @@ export function persistAutoTitleState(
|
|
|
267
254
|
}
|
|
268
255
|
|
|
269
256
|
async function retitleStoredSession(
|
|
270
|
-
|
|
257
|
+
modelRegistry: ExtensionContext["modelRegistry"],
|
|
271
258
|
model: Model<Api>,
|
|
272
259
|
sessionPath: string,
|
|
273
260
|
generation: AutoTitleGeneration,
|
|
@@ -288,7 +275,7 @@ async function retitleStoredSession(
|
|
|
288
275
|
currentTitle,
|
|
289
276
|
};
|
|
290
277
|
const generatedTitle = await generateAutoTitle(
|
|
291
|
-
|
|
278
|
+
modelRegistry,
|
|
292
279
|
model,
|
|
293
280
|
titleContext,
|
|
294
281
|
plan.reason,
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
copyToClipboard,
|
|
4
4
|
type ExtensionAPI,
|
|
5
5
|
type ExtensionCommandContext,
|
|
6
|
-
type ModelRuntime,
|
|
7
6
|
type Theme,
|
|
8
7
|
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import type { Focusable, TUI } from "@earendil-works/pi-tui";
|
|
8
|
+
import type { Focusable, TUI, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
10
9
|
import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
|
+
import { LegendPointer } from "../shared/legend.ts";
|
|
11
11
|
import type { RetitleCommandOutcome, RetitleMode, RetitleScope } from "./command.ts";
|
|
12
12
|
import type { AutoRetitleStatus, SessionAutoTitleController } from "./controller.ts";
|
|
13
13
|
import type { AutoTitleFailure, AutoTitleGeneration } from "./generate.ts";
|
|
@@ -55,7 +55,6 @@ export async function showRetitleWizard(
|
|
|
55
55
|
pi: ExtensionAPI,
|
|
56
56
|
controller: SessionAutoTitleController,
|
|
57
57
|
ctx: ExtensionCommandContext,
|
|
58
|
-
modelRuntime: ModelRuntime,
|
|
59
58
|
model: Model<Api> | undefined,
|
|
60
59
|
getSessionEpoch: () => number,
|
|
61
60
|
generation: AutoTitleGeneration,
|
|
@@ -69,7 +68,6 @@ export async function showRetitleWizard(
|
|
|
69
68
|
pi,
|
|
70
69
|
controller,
|
|
71
70
|
ctx,
|
|
72
|
-
modelRuntime,
|
|
73
71
|
model,
|
|
74
72
|
getSessionEpoch,
|
|
75
73
|
generation,
|
|
@@ -90,6 +88,16 @@ export async function showRetitleWizard(
|
|
|
90
88
|
class RetitleWizardPanel implements Focusable {
|
|
91
89
|
focused = false;
|
|
92
90
|
|
|
91
|
+
private mouseTargets: Array<{
|
|
92
|
+
id: string;
|
|
93
|
+
row: number;
|
|
94
|
+
start: number;
|
|
95
|
+
end: number;
|
|
96
|
+
select: (() => void) | undefined;
|
|
97
|
+
run: () => void;
|
|
98
|
+
}> = [];
|
|
99
|
+
private readonly pointer: LegendPointer;
|
|
100
|
+
private pressedStep: RetitleWizardStep | undefined;
|
|
93
101
|
private selectedIndex = 0;
|
|
94
102
|
private step: RetitleWizardStep;
|
|
95
103
|
|
|
@@ -99,13 +107,13 @@ class RetitleWizardPanel implements Focusable {
|
|
|
99
107
|
private readonly pi: ExtensionAPI,
|
|
100
108
|
private readonly controller: SessionAutoTitleController,
|
|
101
109
|
private readonly ctx: ExtensionCommandContext,
|
|
102
|
-
private readonly modelRuntime: ModelRuntime,
|
|
103
110
|
private readonly model: Model<Api> | undefined,
|
|
104
111
|
private readonly getSessionEpoch: () => number,
|
|
105
112
|
private readonly generation: AutoTitleGeneration,
|
|
106
113
|
private readonly done: (result: RetitleCommandOutcome) => void,
|
|
107
114
|
options?: RetitleWizardOptions,
|
|
108
115
|
) {
|
|
116
|
+
this.pointer = new LegendPointer(() => this.requestRender());
|
|
109
117
|
this.step = options?.initialInvocation
|
|
110
118
|
? { kind: "scan", scope: options.initialInvocation.scope }
|
|
111
119
|
: { kind: "scope" };
|
|
@@ -146,7 +154,29 @@ class RetitleWizardPanel implements Focusable {
|
|
|
146
154
|
}
|
|
147
155
|
}
|
|
148
156
|
|
|
157
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
158
|
+
const target = this.mouseTargets.find(
|
|
159
|
+
(target) => target.row === event.y && event.x >= target.start && event.x < target.end,
|
|
160
|
+
);
|
|
161
|
+
const result = this.pointer.handleMouse(event, target);
|
|
162
|
+
if (event.type === "press") {
|
|
163
|
+
this.pressedStep = this.step;
|
|
164
|
+
if (result?.handled) target?.select?.();
|
|
165
|
+
}
|
|
166
|
+
if (result) return result;
|
|
167
|
+
if (event.type === "wheel" && this.step.kind === "scope" && event.wheelDelta) {
|
|
168
|
+
this.selectedIndex = Math.max(
|
|
169
|
+
0,
|
|
170
|
+
Math.min(2, this.selectedIndex + Math.sign(event.wheelDelta)),
|
|
171
|
+
);
|
|
172
|
+
this.requestRender();
|
|
173
|
+
return { handled: true };
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
149
178
|
render(width: number): string[] {
|
|
179
|
+
this.mouseTargets = [];
|
|
150
180
|
const innerWidth = Math.max(1, width - 2);
|
|
151
181
|
const lines: string[] = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
|
|
152
182
|
|
|
@@ -251,7 +281,6 @@ class RetitleWizardPanel implements Focusable {
|
|
|
251
281
|
pi: this.pi,
|
|
252
282
|
controller: this.controller,
|
|
253
283
|
ctx: this.ctx,
|
|
254
|
-
modelRuntime: this.modelRuntime,
|
|
255
284
|
model: this.model,
|
|
256
285
|
isManual: true,
|
|
257
286
|
generation: this.generation,
|
|
@@ -310,7 +339,6 @@ class RetitleWizardPanel implements Focusable {
|
|
|
310
339
|
this.pi,
|
|
311
340
|
this.controller,
|
|
312
341
|
this.ctx,
|
|
313
|
-
this.modelRuntime,
|
|
314
342
|
this.model,
|
|
315
343
|
scan,
|
|
316
344
|
mode,
|
|
@@ -361,20 +389,23 @@ class RetitleWizardPanel implements Focusable {
|
|
|
361
389
|
);
|
|
362
390
|
lines.push(this.renderRow(innerWidth, ""));
|
|
363
391
|
|
|
364
|
-
for (const option of options) {
|
|
392
|
+
for (const [index, option] of options.entries()) {
|
|
365
393
|
const prefix = option.selected ? this.theme.fg("accent", "›") : " ";
|
|
366
394
|
const text = `${this.theme.bold(option.key)}${option.suffix}`;
|
|
367
395
|
const label = option.selected ? this.theme.fg("accent", text) : text;
|
|
368
|
-
|
|
396
|
+
this.pushActionRow(lines, innerWidth, ` ${prefix} ${label}`, option.key, 2, () => {
|
|
397
|
+
this.selectedIndex = index;
|
|
398
|
+
this.requestRender();
|
|
399
|
+
});
|
|
369
400
|
}
|
|
370
401
|
|
|
371
402
|
if (failure) {
|
|
372
403
|
lines.push(this.renderRow(innerWidth, ""));
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
404
|
+
this.pushActionRow(
|
|
405
|
+
lines,
|
|
406
|
+
innerWidth,
|
|
407
|
+
` ${this.theme.fg("dim", this.theme.bold("y"))}${this.theme.fg("dim", " Copy last auto-title error")}`,
|
|
408
|
+
"y",
|
|
378
409
|
);
|
|
379
410
|
}
|
|
380
411
|
}
|
|
@@ -409,27 +440,27 @@ class RetitleWizardPanel implements Focusable {
|
|
|
409
440
|
lines.push(this.renderRow(innerWidth, ""));
|
|
410
441
|
lines.push(this.renderRow(innerWidth, ` ${message}`));
|
|
411
442
|
lines.push(this.renderRow(innerWidth, ""));
|
|
412
|
-
|
|
413
|
-
|
|
443
|
+
this.pushActionRow(lines, innerWidth, ` ${this.theme.fg("dim", "Enter")} close`, "\r");
|
|
444
|
+
this.pushActionRow(lines, innerWidth, ` ${this.theme.fg("dim", "Esc")} cancel`, "\u001b");
|
|
414
445
|
}
|
|
415
446
|
|
|
416
447
|
private renderConfirmModeStep(lines: string[], innerWidth: number, scan: RetitleScopeScan): void {
|
|
417
448
|
const title = scan.scope === "folder" ? "In This Folder" : "All of Pi";
|
|
418
449
|
lines.push(this.renderRow(innerWidth, ` ${this.theme.bold(this.theme.fg("accent", title))}`));
|
|
419
450
|
lines.push(this.renderRow(innerWidth, ""));
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
451
|
+
this.pushActionRow(
|
|
452
|
+
lines,
|
|
453
|
+
innerWidth,
|
|
454
|
+
` ${this.theme.bold("Enter")} Generate missing titles (${scan.untitledCount})`,
|
|
455
|
+
"\r",
|
|
425
456
|
);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
457
|
+
this.pushActionRow(
|
|
458
|
+
lines,
|
|
459
|
+
innerWidth,
|
|
460
|
+
` ${this.theme.bold("a")} Regenerate all sessions, even ones that already have titles (${scan.totalCount})`,
|
|
461
|
+
"a",
|
|
431
462
|
);
|
|
432
|
-
|
|
463
|
+
this.pushActionRow(lines, innerWidth, ` ${this.theme.fg("dim", "Esc")} cancel`, "\u001b");
|
|
433
464
|
}
|
|
434
465
|
|
|
435
466
|
private renderWarningStep(lines: string[], innerWidth: number, sessionCount: number): void {
|
|
@@ -442,8 +473,8 @@ class RetitleWizardPanel implements Focusable {
|
|
|
442
473
|
);
|
|
443
474
|
lines.push(this.renderRow(innerWidth, " This may take some time and may be expensive."));
|
|
444
475
|
lines.push(this.renderRow(innerWidth, ""));
|
|
445
|
-
|
|
446
|
-
|
|
476
|
+
this.pushActionRow(lines, innerWidth, ` ${this.theme.fg("accent", "Enter")} continue`, "\r");
|
|
477
|
+
this.pushActionRow(lines, innerWidth, ` ${this.theme.fg("dim", "Esc")} cancel`, "\u001b");
|
|
447
478
|
}
|
|
448
479
|
|
|
449
480
|
private renderRunningStep(lines: string[], innerWidth: number, message: string): void {
|
|
@@ -481,9 +512,44 @@ class RetitleWizardPanel implements Focusable {
|
|
|
481
512
|
return this.renderRow(innerWidth, `${left}${" ".repeat(gap)}${right}`);
|
|
482
513
|
}
|
|
483
514
|
|
|
515
|
+
private pushActionRow(
|
|
516
|
+
lines: string[],
|
|
517
|
+
innerWidth: number,
|
|
518
|
+
content: string,
|
|
519
|
+
key: string,
|
|
520
|
+
start = 1,
|
|
521
|
+
select?: () => void,
|
|
522
|
+
): void {
|
|
523
|
+
const step = this.step;
|
|
524
|
+
const visible = visibleWidth(content) <= innerWidth;
|
|
525
|
+
if (visible) {
|
|
526
|
+
this.mouseTargets.push({
|
|
527
|
+
id: key,
|
|
528
|
+
row: lines.length,
|
|
529
|
+
start: start + 1,
|
|
530
|
+
end: visibleWidth(content) + 1,
|
|
531
|
+
select,
|
|
532
|
+
run: () => {
|
|
533
|
+
if (this.step === step) this.handleInput(key);
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
const pressed =
|
|
538
|
+
visible && !select && this.pressedStep === step && this.pointer.pressedId === key;
|
|
539
|
+
lines.push(
|
|
540
|
+
this.renderRow(
|
|
541
|
+
innerWidth,
|
|
542
|
+
pressed
|
|
543
|
+
? content.slice(0, start) + this.theme.bg("selectedBg", content.slice(start))
|
|
544
|
+
: content,
|
|
545
|
+
),
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
484
549
|
private renderRow(innerWidth: number, content: string): string {
|
|
485
|
-
const
|
|
486
|
-
|
|
550
|
+
const text = truncateToWidth(content, innerWidth, "…");
|
|
551
|
+
const pad = Math.max(0, innerWidth - visibleWidth(text));
|
|
552
|
+
return `${this.theme.fg("border", "│")}${text}${" ".repeat(pad)}${this.theme.fg("border", "│")}`;
|
|
487
553
|
}
|
|
488
554
|
|
|
489
555
|
private requestRender(): void {
|