pi-ui-extend 0.1.32 → 0.1.34
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 +1 -1
- package/dist/app/app.d.ts +2 -0
- package/dist/app/app.js +28 -0
- package/dist/app/commands/command-session-actions.js +29 -1
- package/dist/app/constants.d.ts +1 -1
- package/dist/app/constants.js +2 -2
- package/dist/app/icons.d.ts +4 -9
- package/dist/app/icons.js +12 -35
- package/dist/app/model/model-usage-status.d.ts +2 -1
- package/dist/app/model/model-usage-status.js +33 -25
- package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-tool-renderer.js +12 -18
- package/dist/app/rendering/conversation-viewport.d.ts +4 -0
- package/dist/app/rendering/conversation-viewport.js +144 -13
- package/dist/app/rendering/dcp-stats.js +42 -16
- package/dist/app/rendering/render-controller.js +4 -0
- package/dist/app/rendering/status-line-renderer.d.ts +8 -1
- package/dist/app/rendering/status-line-renderer.js +36 -1
- package/dist/app/rendering/tab-line-renderer.js +2 -2
- package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
- package/dist/app/rendering/tool-block-renderer.js +37 -11
- package/dist/app/runtime.js +1 -1
- package/dist/app/screen/mouse-controller.d.ts +5 -1
- package/dist/app/screen/mouse-controller.js +16 -0
- package/dist/app/screen/scroll-controller.d.ts +20 -0
- package/dist/app/screen/scroll-controller.js +127 -10
- package/dist/app/session/lazy-session-manager.js +35 -5
- package/dist/app/session/pix-system-message.d.ts +1 -0
- package/dist/app/session/pix-system-message.js +14 -3
- package/dist/app/session/queued-message-controller.d.ts +11 -4
- package/dist/app/session/queued-message-controller.js +74 -59
- package/dist/app/session/queued-message-entries.d.ts +2 -1
- package/dist/app/session/queued-message-entries.js +12 -1
- package/dist/app/session/session-event-controller.d.ts +42 -1
- package/dist/app/session/session-event-controller.js +500 -31
- package/dist/app/session/session-history.js +23 -4
- package/dist/app/session/tabs-controller.d.ts +11 -1
- package/dist/app/session/tabs-controller.js +102 -21
- package/dist/app/types.d.ts +14 -1
- package/dist/bundled-extensions/question/contract.d.ts +25 -0
- package/dist/bundled-extensions/question/contract.js +94 -0
- package/dist/bundled-extensions/question/index.d.ts +7 -0
- package/dist/bundled-extensions/question/index.js +28 -0
- package/dist/bundled-extensions/question/render.d.ts +4 -0
- package/dist/bundled-extensions/question/render.js +27 -0
- package/dist/bundled-extensions/question/result.d.ts +6 -0
- package/dist/bundled-extensions/question/result.js +84 -0
- package/dist/bundled-extensions/question/tool-description.d.ts +7 -0
- package/dist/bundled-extensions/question/tool-description.js +11 -0
- package/dist/bundled-extensions/question/tui.d.ts +2 -0
- package/dist/bundled-extensions/question/tui.js +577 -0
- package/dist/bundled-extensions/question/types.d.ts +103 -0
- package/dist/bundled-extensions/question/types.js +1 -0
- package/dist/bundled-extensions/session-title/config.d.ts +17 -0
- package/dist/bundled-extensions/session-title/config.js +150 -0
- package/dist/bundled-extensions/session-title/index.d.ts +5 -0
- package/dist/bundled-extensions/session-title/index.js +384 -0
- package/dist/bundled-extensions/session-title/title-generation.d.ts +26 -0
- package/dist/bundled-extensions/session-title/title-generation.js +141 -0
- package/dist/bundled-extensions/terminal-bell/index.d.ts +14 -0
- package/dist/bundled-extensions/terminal-bell/index.js +491 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -1
- package/dist/default-pix-config.js +2 -1
- package/dist/icon-theme.d.ts +7 -0
- package/dist/icon-theme.js +36 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +4 -0
- package/dist/schemas/pi-tools-suite-schema.js +5 -0
- package/dist/schemas/pix-schema.d.ts +1 -0
- package/dist/schemas/pix-schema.js +1 -0
- package/external/pi-tools-suite/README.md +7 -7
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +16 -16
- package/external/pi-tools-suite/src/async-subagents/core/state.ts +18 -4
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +4 -0
- package/external/pi-tools-suite/src/async-subagents/tools/result.ts +14 -26
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +0 -1
- package/external/pi-tools-suite/src/dcp/config.ts +14 -14
- package/external/pi-tools-suite/src/dcp/index.ts +31 -43
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +151 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +25 -18
- package/external/pi-tools-suite/src/tool-descriptions.ts +34 -54
- package/package.json +3 -2
- package/schemas/pi-tools-suite.json +14 -0
- package/schemas/pix.json +7 -0
- package/extensions/question/contract.ts +0 -100
- package/extensions/question/index.ts +0 -34
- package/extensions/question/render.ts +0 -28
- package/extensions/question/result.ts +0 -86
- package/extensions/question/tool-description.ts +0 -11
- package/extensions/question/tui.ts +0 -629
- package/extensions/question/types.ts +0 -123
- package/extensions/session-title/config.ts +0 -164
- package/extensions/session-title/index.ts +0 -502
- package/extensions/terminal-bell/index.ts +0 -345
|
@@ -2,7 +2,8 @@ import { isRecord } from "../guards.js";
|
|
|
2
2
|
import { createId } from "../id.js";
|
|
3
3
|
import { isOnlyHiddenMetadata } from "../../markdown-format.js";
|
|
4
4
|
import { extractImageContents, renderContent, renderUserMessageContent, stringifyUnknown } from "../rendering/message-content.js";
|
|
5
|
-
import {
|
|
5
|
+
import { THINKING_TOOL_NAME } from "../constants.js";
|
|
6
|
+
import { PIX_SESSION_ENTRY_ID_FIELD, PIX_SYSTEM_MESSAGE_CUSTOM_TYPE, PIX_THINKING_LEVEL_FIELD } from "./pix-system-message.js";
|
|
6
7
|
const HISTORICAL_SUBAGENTS_OBSERVATION = { showSnapshot: false };
|
|
7
8
|
const DEFAULT_HISTORY_CHUNK_SIZE = 50;
|
|
8
9
|
const DEFAULT_HISTORY_TAIL_MESSAGE_COUNT = 80;
|
|
@@ -178,6 +179,9 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
|
|
|
178
179
|
const content = message.content;
|
|
179
180
|
if (!Array.isArray(content))
|
|
180
181
|
return;
|
|
182
|
+
const messageThinkingLevel = typeof message[PIX_THINKING_LEVEL_FIELD] === "string"
|
|
183
|
+
? message[PIX_THINKING_LEVEL_FIELD]
|
|
184
|
+
: undefined;
|
|
181
185
|
let assistantText = "";
|
|
182
186
|
let thinkingText = "";
|
|
183
187
|
for (const block of content) {
|
|
@@ -186,7 +190,14 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
|
|
|
186
190
|
if (block.type === "toolCall") {
|
|
187
191
|
// Flush accumulated text/thinking before tool call.
|
|
188
192
|
if (thinkingText) {
|
|
189
|
-
options.addEntry({
|
|
193
|
+
options.addEntry({
|
|
194
|
+
id: createId("thinking"),
|
|
195
|
+
kind: "thinking",
|
|
196
|
+
text: thinkingText,
|
|
197
|
+
expanded: options.toolDefaultExpanded(THINKING_TOOL_NAME),
|
|
198
|
+
...(messageThinkingLevel === undefined ? {} : { level: messageThinkingLevel }),
|
|
199
|
+
status: "done",
|
|
200
|
+
});
|
|
190
201
|
thinkingText = "";
|
|
191
202
|
}
|
|
192
203
|
if (assistantText && !isOnlyHiddenMetadata(assistantText)) {
|
|
@@ -195,6 +206,7 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
|
|
|
195
206
|
assistantText = "";
|
|
196
207
|
const toolCallId = String(block.id ?? createId("tool"));
|
|
197
208
|
const result = toolResults.get(toolCallId);
|
|
209
|
+
const isPendingToolCall = result === undefined && message.stopReason !== "aborted" && message.stopReason !== "error";
|
|
198
210
|
const toolName = result?.toolName ?? String(block.name ?? "unknown");
|
|
199
211
|
const argsText = stringifyUnknown(block.arguments);
|
|
200
212
|
const output = result ? renderContent(result.content) : "";
|
|
@@ -213,7 +225,7 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
|
|
|
213
225
|
...(result?.details === undefined ? {} : { details: result.details }),
|
|
214
226
|
expanded: options.toolDefaultExpanded(toolName),
|
|
215
227
|
isError: result?.isError ?? false,
|
|
216
|
-
status: "done",
|
|
228
|
+
status: isPendingToolCall ? "running" : "done",
|
|
217
229
|
});
|
|
218
230
|
options.setToolEntryId(toolCallId, entryId);
|
|
219
231
|
}
|
|
@@ -226,7 +238,14 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
|
|
|
226
238
|
}
|
|
227
239
|
// Flush remaining text.
|
|
228
240
|
if (thinkingText) {
|
|
229
|
-
options.addEntry({
|
|
241
|
+
options.addEntry({
|
|
242
|
+
id: createId("thinking"),
|
|
243
|
+
kind: "thinking",
|
|
244
|
+
text: thinkingText,
|
|
245
|
+
expanded: options.toolDefaultExpanded(THINKING_TOOL_NAME),
|
|
246
|
+
...(messageThinkingLevel === undefined ? {} : { level: messageThinkingLevel }),
|
|
247
|
+
status: "done",
|
|
248
|
+
});
|
|
230
249
|
}
|
|
231
250
|
if (assistantText && !isOnlyHiddenMetadata(assistantText)) {
|
|
232
251
|
options.addEntry({ id: createId("assistant"), kind: "assistant", text: assistantText });
|
|
@@ -3,10 +3,12 @@ import type { BindCurrentSessionOptions } from "./session-lifecycle-controller.j
|
|
|
3
3
|
import type { AppSessionEventControllerState } from "./session-event-controller.js";
|
|
4
4
|
import type { InputEditorDraftState } from "../../input-editor.js";
|
|
5
5
|
import type { AppBlinkController } from "../screen/blink-controller.js";
|
|
6
|
+
import type { AppScrollState } from "../screen/scroll-controller.js";
|
|
6
7
|
import type { AppOptions, Entry, SessionActivity, SessionTab, SubmittedUserMessage } from "../types.js";
|
|
7
8
|
type TabSessionView = {
|
|
8
9
|
entries: Entry[];
|
|
9
10
|
eventState: AppSessionEventControllerState;
|
|
11
|
+
scrollState: AppScrollState;
|
|
10
12
|
};
|
|
11
13
|
export type TabInputState = InputEditorDraftState;
|
|
12
14
|
export type AppTabsControllerHost = {
|
|
@@ -31,10 +33,13 @@ export type AppTabsControllerHost = {
|
|
|
31
33
|
}): Promise<boolean>;
|
|
32
34
|
captureSessionView?(): TabSessionView;
|
|
33
35
|
restoreSessionView?(view: TabSessionView): void;
|
|
36
|
+
restoreScrollState?(state: AppScrollState): void;
|
|
34
37
|
syncUserSessionEntryMetadata(): void;
|
|
35
38
|
captureInputState(): TabInputState;
|
|
36
39
|
restoreInputState(state: TabInputState): void;
|
|
37
40
|
closeMenusForTabSwitch?(): void;
|
|
41
|
+
captureAutoUserMessages?(): readonly SubmittedUserMessage[];
|
|
42
|
+
restoreAutoUserMessages?(messages: readonly SubmittedUserMessage[]): void;
|
|
38
43
|
captureDeferredUserMessages?(): readonly SubmittedUserMessage[];
|
|
39
44
|
restoreDeferredUserMessages?(messages: readonly SubmittedUserMessage[]): void;
|
|
40
45
|
addEntry(entry: Entry): void;
|
|
@@ -50,8 +55,10 @@ export declare class AppTabsController {
|
|
|
50
55
|
private readonly runtimeRefreshTimersByTabId;
|
|
51
56
|
private readonly historyReloadTimersByTabId;
|
|
52
57
|
private readonly inputStatesByTabId;
|
|
58
|
+
private readonly autoUserMessagesByTabId;
|
|
53
59
|
private readonly deferredUserMessagesByTabId;
|
|
54
60
|
private readonly sessionViewsByTabId;
|
|
61
|
+
private readonly scrollStatesByTabId;
|
|
55
62
|
private readonly tabIdsNeedingHistoryReload;
|
|
56
63
|
private activeTabId;
|
|
57
64
|
private pendingActiveTabId;
|
|
@@ -92,6 +99,7 @@ export declare class AppTabsController {
|
|
|
92
99
|
private clearStartupTabPlaceholders;
|
|
93
100
|
private storeActiveRuntime;
|
|
94
101
|
private storeActiveSessionView;
|
|
102
|
+
private restoreStoredScrollState;
|
|
95
103
|
private setRuntimeForTab;
|
|
96
104
|
private deleteRuntimeForTab;
|
|
97
105
|
private clearRuntimeSubscriptions;
|
|
@@ -116,7 +124,8 @@ export declare class AppTabsController {
|
|
|
116
124
|
private replaceTabs;
|
|
117
125
|
private removeTab;
|
|
118
126
|
private restorePersistedInputStates;
|
|
119
|
-
private
|
|
127
|
+
private restorePersistedScrollStates;
|
|
128
|
+
private restorePersistedQueuedUserMessages;
|
|
120
129
|
private ensureCurrentSessionTab;
|
|
121
130
|
private tabFromSession;
|
|
122
131
|
private updateTabFromSession;
|
|
@@ -136,6 +145,7 @@ export declare class AppTabsController {
|
|
|
136
145
|
private refreshRestoredTabTitles;
|
|
137
146
|
private loadTabs;
|
|
138
147
|
private parsePersistedInputState;
|
|
148
|
+
private parsePersistedScrollState;
|
|
139
149
|
private parsePersistedSubmittedUserMessages;
|
|
140
150
|
private saveTabs;
|
|
141
151
|
private filePath;
|
|
@@ -6,12 +6,12 @@ import { getAgentDir, } from "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
import { isRecord } from "../guards.js";
|
|
7
7
|
import { createId } from "../id.js";
|
|
8
8
|
import { tabPanelRows } from "../rendering/tab-line-renderer.js";
|
|
9
|
-
const TAB_STATE_VERSION =
|
|
9
|
+
const TAB_STATE_VERSION = 4;
|
|
10
10
|
const MAX_RESTORED_TABS = 8;
|
|
11
11
|
const BACKGROUND_PREWARM_TAB_LIMIT = 2;
|
|
12
12
|
const TAB_ATTENTION_BLINK_KEY = "tab-attention";
|
|
13
13
|
const LOADING_TAB_TITLE_PATTERN = /^loading(?:…|\.\.\.)?$/iu;
|
|
14
|
-
const DEFAULT_SESSION_TITLE_PATTERN = /^session [0-9a-f]{8}$/iu;
|
|
14
|
+
const DEFAULT_SESSION_TITLE_PATTERN = /^(?:session )?[0-9a-f]{8}$/iu;
|
|
15
15
|
const SESSION_TITLE_HEAD_SCAN_MAX_BYTES = 256 * 1024;
|
|
16
16
|
const SESSION_TITLE_SCAN_MAX_BYTES = 2 * 1024 * 1024;
|
|
17
17
|
export class AppTabsController {
|
|
@@ -23,8 +23,10 @@ export class AppTabsController {
|
|
|
23
23
|
runtimeRefreshTimersByTabId = new Map();
|
|
24
24
|
historyReloadTimersByTabId = new Map();
|
|
25
25
|
inputStatesByTabId = new Map();
|
|
26
|
+
autoUserMessagesByTabId = new Map();
|
|
26
27
|
deferredUserMessagesByTabId = new Map();
|
|
27
28
|
sessionViewsByTabId = new Map();
|
|
29
|
+
scrollStatesByTabId = new Map();
|
|
28
30
|
tabIdsNeedingHistoryReload = new Set();
|
|
29
31
|
activeTabId;
|
|
30
32
|
pendingActiveTabId;
|
|
@@ -195,7 +197,7 @@ export class AppTabsController {
|
|
|
195
197
|
: restoredTabs[0]?.sessionPath;
|
|
196
198
|
this.replaceTabs(restoredTabs, desiredPath);
|
|
197
199
|
this.restorePersistedInputStates(saved);
|
|
198
|
-
this.
|
|
200
|
+
this.restorePersistedQueuedUserMessages(saved);
|
|
199
201
|
const restoredSessionPaths = saved.tabs.map((tab) => tab.path);
|
|
200
202
|
if (explicitSessionPath && currentPath)
|
|
201
203
|
this.ensureCurrentSessionTab(runtime.session);
|
|
@@ -230,6 +232,7 @@ export class AppTabsController {
|
|
|
230
232
|
this.clearStartupTabPlaceholders();
|
|
231
233
|
if (this.activeTabId)
|
|
232
234
|
this.restoreInputState(this.activeTabId);
|
|
235
|
+
this.restorePersistedScrollStates(saved);
|
|
233
236
|
await this.saveTabs();
|
|
234
237
|
this.scheduleProjectSessionRetention();
|
|
235
238
|
this.scheduleTabPrewarm();
|
|
@@ -248,6 +251,7 @@ export class AppTabsController {
|
|
|
248
251
|
return;
|
|
249
252
|
this.cancelHistoryLoad();
|
|
250
253
|
this.syncActiveTabFromRuntime();
|
|
254
|
+
this.storeActiveSessionView();
|
|
251
255
|
this.storeActiveInputState();
|
|
252
256
|
this.storeActiveDeferredUserMessages();
|
|
253
257
|
const previousTabId = this.activeTabId;
|
|
@@ -347,6 +351,7 @@ export class AppTabsController {
|
|
|
347
351
|
return true;
|
|
348
352
|
}
|
|
349
353
|
this.cancelHistoryLoad();
|
|
354
|
+
this.storeActiveSessionView();
|
|
350
355
|
this.storeActiveInputState();
|
|
351
356
|
this.storeActiveDeferredUserMessages();
|
|
352
357
|
const previousTabId = this.activeTabId;
|
|
@@ -455,6 +460,7 @@ export class AppTabsController {
|
|
|
455
460
|
}
|
|
456
461
|
this.cancelHistoryLoad();
|
|
457
462
|
this.syncActiveTabFromRuntime({ save: false });
|
|
463
|
+
this.storeActiveSessionView();
|
|
458
464
|
this.storeActiveInputState();
|
|
459
465
|
this.storeActiveDeferredUserMessages();
|
|
460
466
|
const previousTabId = this.activeTabId;
|
|
@@ -670,6 +676,7 @@ export class AppTabsController {
|
|
|
670
676
|
this.tabItems.splice(index, 1);
|
|
671
677
|
this.deleteRuntimeForTab(tabId);
|
|
672
678
|
this.inputStatesByTabId.delete(tabId);
|
|
679
|
+
this.autoUserMessagesByTabId.delete(tabId);
|
|
673
680
|
this.deferredUserMessagesByTabId.delete(tabId);
|
|
674
681
|
this.storeActiveInputState();
|
|
675
682
|
this.storeActiveDeferredUserMessages();
|
|
@@ -697,6 +704,7 @@ export class AppTabsController {
|
|
|
697
704
|
this.tabItems.splice(index, 1);
|
|
698
705
|
this.deleteRuntimeForTab(tabId);
|
|
699
706
|
this.inputStatesByTabId.delete(tabId);
|
|
707
|
+
this.autoUserMessagesByTabId.delete(tabId);
|
|
700
708
|
this.deferredUserMessagesByTabId.delete(tabId);
|
|
701
709
|
this.stopAttentionBlinkIfIdle();
|
|
702
710
|
this.activeTabId = nextTab.id;
|
|
@@ -732,6 +740,7 @@ export class AppTabsController {
|
|
|
732
740
|
this.updateTabFromSession(tab, runtime.session);
|
|
733
741
|
this.setRuntimeForTab(tab.id, runtime);
|
|
734
742
|
this.inputStatesByTabId.delete(tab.id);
|
|
743
|
+
this.autoUserMessagesByTabId.delete(tab.id);
|
|
735
744
|
this.deferredUserMessagesByTabId.delete(tab.id);
|
|
736
745
|
this.restoreInputState(tab.id);
|
|
737
746
|
this.host.closeMenusForTabSwitch?.();
|
|
@@ -770,6 +779,7 @@ export class AppTabsController {
|
|
|
770
779
|
});
|
|
771
780
|
if (!completed || isCancelled())
|
|
772
781
|
return;
|
|
782
|
+
this.restoreStoredScrollState(this.activeTabId);
|
|
773
783
|
this.host.setSessionStatus(runtime.session);
|
|
774
784
|
this.host.syncUserSessionEntryMetadata();
|
|
775
785
|
this.host.setSessionActivity(this.sessionActivity(runtime.session));
|
|
@@ -824,7 +834,17 @@ export class AppTabsController {
|
|
|
824
834
|
storeActiveSessionView() {
|
|
825
835
|
if (!this.activeTabId || !this.host.captureSessionView)
|
|
826
836
|
return;
|
|
827
|
-
|
|
837
|
+
const view = this.host.captureSessionView();
|
|
838
|
+
this.sessionViewsByTabId.set(this.activeTabId, view);
|
|
839
|
+
this.scrollStatesByTabId.set(this.activeTabId, view.scrollState);
|
|
840
|
+
}
|
|
841
|
+
restoreStoredScrollState(tabId) {
|
|
842
|
+
if (!tabId || !this.host.restoreScrollState)
|
|
843
|
+
return;
|
|
844
|
+
const scrollState = this.scrollStatesByTabId.get(tabId);
|
|
845
|
+
if (!scrollState)
|
|
846
|
+
return;
|
|
847
|
+
this.host.restoreScrollState(scrollState);
|
|
828
848
|
}
|
|
829
849
|
setRuntimeForTab(tabId, runtime) {
|
|
830
850
|
this.runtimesByTabId.set(tabId, runtime);
|
|
@@ -834,6 +854,7 @@ export class AppTabsController {
|
|
|
834
854
|
this.runtimesByTabId.delete(tabId);
|
|
835
855
|
this.runtimeLoadsByTabId.delete(tabId);
|
|
836
856
|
this.sessionViewsByTabId.delete(tabId);
|
|
857
|
+
this.scrollStatesByTabId.delete(tabId);
|
|
837
858
|
this.clearRuntimeRefreshTimers(tabId);
|
|
838
859
|
this.clearHistoryReloadTimers(tabId);
|
|
839
860
|
this.tabIdsNeedingHistoryReload.delete(tabId);
|
|
@@ -977,11 +998,18 @@ export class AppTabsController {
|
|
|
977
998
|
});
|
|
978
999
|
}
|
|
979
1000
|
storeActiveDeferredUserMessages() {
|
|
980
|
-
if (!this.activeTabId
|
|
1001
|
+
if (!this.activeTabId)
|
|
981
1002
|
return;
|
|
982
|
-
const
|
|
983
|
-
if (
|
|
984
|
-
this.
|
|
1003
|
+
const autoMessages = this.host.captureAutoUserMessages?.() ?? [];
|
|
1004
|
+
if (autoMessages.length > 0) {
|
|
1005
|
+
this.autoUserMessagesByTabId.set(this.activeTabId, autoMessages.map((message) => this.cloneSubmittedUserMessage(message)));
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
this.autoUserMessagesByTabId.delete(this.activeTabId);
|
|
1009
|
+
}
|
|
1010
|
+
const deferredMessages = this.host.captureDeferredUserMessages?.() ?? [];
|
|
1011
|
+
if (deferredMessages.length > 0) {
|
|
1012
|
+
this.deferredUserMessagesByTabId.set(this.activeTabId, deferredMessages.map((message) => this.cloneSubmittedUserMessage(message)));
|
|
985
1013
|
}
|
|
986
1014
|
else {
|
|
987
1015
|
this.deferredUserMessagesByTabId.delete(this.activeTabId);
|
|
@@ -994,6 +1022,7 @@ export class AppTabsController {
|
|
|
994
1022
|
return { text, cursor: text.length };
|
|
995
1023
|
}
|
|
996
1024
|
restoreDeferredUserMessages(tabId) {
|
|
1025
|
+
this.host.restoreAutoUserMessages?.(this.autoUserMessagesByTabId.get(tabId) ?? []);
|
|
997
1026
|
this.host.restoreDeferredUserMessages?.(this.deferredUserMessagesByTabId.get(tabId) ?? []);
|
|
998
1027
|
}
|
|
999
1028
|
cloneSubmittedUserMessage(message) {
|
|
@@ -1050,8 +1079,10 @@ export class AppTabsController {
|
|
|
1050
1079
|
this.runtimesByTabId.clear();
|
|
1051
1080
|
this.clearRuntimeSubscriptions();
|
|
1052
1081
|
this.inputStatesByTabId.clear();
|
|
1082
|
+
this.autoUserMessagesByTabId.clear();
|
|
1053
1083
|
this.deferredUserMessagesByTabId.clear();
|
|
1054
1084
|
this.sessionViewsByTabId.clear();
|
|
1085
|
+
this.scrollStatesByTabId.clear();
|
|
1055
1086
|
const seen = new Set();
|
|
1056
1087
|
for (const tab of tabs) {
|
|
1057
1088
|
const sessionPath = tab.sessionPath ? resolve(tab.sessionPath) : undefined;
|
|
@@ -1081,6 +1112,7 @@ export class AppTabsController {
|
|
|
1081
1112
|
this.tabItems.splice(index, 1);
|
|
1082
1113
|
this.deleteRuntimeForTab(tabId);
|
|
1083
1114
|
this.inputStatesByTabId.delete(tabId);
|
|
1115
|
+
this.autoUserMessagesByTabId.delete(tabId);
|
|
1084
1116
|
this.deferredUserMessagesByTabId.delete(tabId);
|
|
1085
1117
|
}
|
|
1086
1118
|
restorePersistedInputStates(saved) {
|
|
@@ -1099,20 +1131,44 @@ export class AppTabsController {
|
|
|
1099
1131
|
this.inputStatesByTabId.set(tab.id, input);
|
|
1100
1132
|
}
|
|
1101
1133
|
}
|
|
1102
|
-
|
|
1103
|
-
const
|
|
1134
|
+
restorePersistedScrollStates(saved) {
|
|
1135
|
+
const scrollStatesByPath = new Map();
|
|
1104
1136
|
for (const tab of saved.tabs) {
|
|
1105
|
-
if (!tab.
|
|
1137
|
+
if (!tab.scrollState)
|
|
1106
1138
|
continue;
|
|
1107
|
-
|
|
1139
|
+
scrollStatesByPath.set(resolve(tab.path), tab.scrollState);
|
|
1108
1140
|
}
|
|
1109
1141
|
for (const tab of this.tabItems) {
|
|
1110
1142
|
if (!tab.sessionPath)
|
|
1111
1143
|
continue;
|
|
1112
|
-
const
|
|
1113
|
-
if (!
|
|
1144
|
+
const scrollState = scrollStatesByPath.get(resolve(tab.sessionPath));
|
|
1145
|
+
if (!scrollState)
|
|
1114
1146
|
continue;
|
|
1115
|
-
this.
|
|
1147
|
+
this.scrollStatesByTabId.set(tab.id, scrollState);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
restorePersistedQueuedUserMessages(saved) {
|
|
1151
|
+
const autoMessagesByPath = new Map();
|
|
1152
|
+
const deferredMessagesByPath = new Map();
|
|
1153
|
+
for (const tab of saved.tabs) {
|
|
1154
|
+
const sessionPath = resolve(tab.path);
|
|
1155
|
+
if (tab.autoUserMessages && tab.autoUserMessages.length > 0) {
|
|
1156
|
+
autoMessagesByPath.set(sessionPath, tab.autoUserMessages.map((message) => this.cloneSubmittedUserMessage(message)));
|
|
1157
|
+
}
|
|
1158
|
+
if (tab.deferredUserMessages && tab.deferredUserMessages.length > 0) {
|
|
1159
|
+
deferredMessagesByPath.set(sessionPath, tab.deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message)));
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
for (const tab of this.tabItems) {
|
|
1163
|
+
if (!tab.sessionPath)
|
|
1164
|
+
continue;
|
|
1165
|
+
const sessionPath = resolve(tab.sessionPath);
|
|
1166
|
+
const autoMessages = autoMessagesByPath.get(sessionPath);
|
|
1167
|
+
if (autoMessages && autoMessages.length > 0)
|
|
1168
|
+
this.autoUserMessagesByTabId.set(tab.id, autoMessages);
|
|
1169
|
+
const deferredMessages = deferredMessagesByPath.get(sessionPath);
|
|
1170
|
+
if (deferredMessages && deferredMessages.length > 0)
|
|
1171
|
+
this.deferredUserMessagesByTabId.set(tab.id, deferredMessages);
|
|
1116
1172
|
}
|
|
1117
1173
|
}
|
|
1118
1174
|
ensureCurrentSessionTab(session) {
|
|
@@ -1157,7 +1213,7 @@ export class AppTabsController {
|
|
|
1157
1213
|
}
|
|
1158
1214
|
sessionTitleFromParts(sessionId, sessionName) {
|
|
1159
1215
|
const name = sessionName?.trim();
|
|
1160
|
-
return name && !LOADING_TAB_TITLE_PATTERN.test(name) ? name :
|
|
1216
|
+
return name && !LOADING_TAB_TITLE_PATTERN.test(name) ? name : sessionId.slice(0, 8);
|
|
1161
1217
|
}
|
|
1162
1218
|
updatedSessionTitle(currentTitle, nextTitle, currentSessionPath, nextSessionPath, hasTitlePlaceholder) {
|
|
1163
1219
|
if (!isDefaultSessionTitle(nextTitle))
|
|
@@ -1204,8 +1260,8 @@ export class AppTabsController {
|
|
|
1204
1260
|
for (const tab of saved.tabs) {
|
|
1205
1261
|
const sessionPath = resolve(tab.path);
|
|
1206
1262
|
const hasDraftInput = (tab.input?.text.length ?? 0) > 0;
|
|
1207
|
-
const
|
|
1208
|
-
if (seen.has(sessionPath) || (!existsSync(sessionPath) && !hasDraftInput && !
|
|
1263
|
+
const hasQueuedMessages = (tab.autoUserMessages?.length ?? 0) > 0 || (tab.deferredUserMessages?.length ?? 0) > 0;
|
|
1264
|
+
if (seen.has(sessionPath) || (!existsSync(sessionPath) && !hasDraftInput && !hasQueuedMessages))
|
|
1209
1265
|
continue;
|
|
1210
1266
|
seen.add(sessionPath);
|
|
1211
1267
|
const savedTitle = tab.title?.trim();
|
|
@@ -1227,7 +1283,7 @@ export class AppTabsController {
|
|
|
1227
1283
|
const fileName = basename(sessionPath, extname(sessionPath));
|
|
1228
1284
|
const sessionId = /^[0-9a-f]{8}/iu.exec(fileName)?.[0]?.toLowerCase()
|
|
1229
1285
|
?? createHash("sha256").update(sessionPath).digest("hex").slice(0, 8);
|
|
1230
|
-
return
|
|
1286
|
+
return sessionId;
|
|
1231
1287
|
}
|
|
1232
1288
|
async loadSessionTitles(sessionPaths) {
|
|
1233
1289
|
const uniquePaths = [...new Set(sessionPaths.map((sessionPath) => resolve(sessionPath)))].slice(0, MAX_RESTORED_TABS);
|
|
@@ -1267,23 +1323,27 @@ export class AppTabsController {
|
|
|
1267
1323
|
try {
|
|
1268
1324
|
const raw = await readFile(this.filePath(), "utf8");
|
|
1269
1325
|
const parsed = JSON.parse(raw);
|
|
1270
|
-
if (!isRecord(parsed) || (parsed.version !== 1 && parsed.version !== 2 && parsed.version !== TAB_STATE_VERSION) || !Array.isArray(parsed.tabs))
|
|
1326
|
+
if (!isRecord(parsed) || (parsed.version !== 1 && parsed.version !== 2 && parsed.version !== 3 && parsed.version !== TAB_STATE_VERSION) || !Array.isArray(parsed.tabs))
|
|
1271
1327
|
return undefined;
|
|
1272
1328
|
const tabs = [];
|
|
1273
1329
|
for (const value of parsed.tabs) {
|
|
1274
1330
|
if (!isRecord(value) || typeof value.path !== "string")
|
|
1275
1331
|
continue;
|
|
1276
1332
|
const input = this.parsePersistedInputState(value.input);
|
|
1333
|
+
const scrollState = this.parsePersistedScrollState(value.scrollState);
|
|
1334
|
+
const autoUserMessages = this.parsePersistedSubmittedUserMessages(value.autoUserMessages);
|
|
1277
1335
|
const deferredUserMessages = this.parsePersistedSubmittedUserMessages(value.deferredUserMessages);
|
|
1278
1336
|
tabs.push({
|
|
1279
1337
|
path: value.path,
|
|
1280
1338
|
...(typeof value.title === "string" ? { title: value.title } : {}),
|
|
1281
1339
|
...(input ? { input } : {}),
|
|
1340
|
+
...(scrollState ? { scrollState } : {}),
|
|
1341
|
+
...(autoUserMessages.length > 0 ? { autoUserMessages } : {}),
|
|
1282
1342
|
...(deferredUserMessages.length > 0 ? { deferredUserMessages } : {}),
|
|
1283
1343
|
});
|
|
1284
1344
|
}
|
|
1285
1345
|
return {
|
|
1286
|
-
version: parsed.version === 1 ? 1 : parsed.version === 2 ? 2 : TAB_STATE_VERSION,
|
|
1346
|
+
version: parsed.version === 1 ? 1 : parsed.version === 2 ? 2 : parsed.version === 3 ? 3 : TAB_STATE_VERSION,
|
|
1287
1347
|
cwd: typeof parsed.cwd === "string" ? parsed.cwd : this.host.options.cwd,
|
|
1288
1348
|
tabs,
|
|
1289
1349
|
...(typeof parsed.activePath === "string" ? { activePath: parsed.activePath } : {}),
|
|
@@ -1306,6 +1366,18 @@ export class AppTabsController {
|
|
|
1306
1366
|
: [];
|
|
1307
1367
|
return { text: value.text, cursor, ...(attachments.length > 0 ? { attachments } : {}) };
|
|
1308
1368
|
}
|
|
1369
|
+
parsePersistedScrollState(value) {
|
|
1370
|
+
if (!isRecord(value) || typeof value.scrollFromBottom !== "number" || !Number.isFinite(value.scrollFromBottom))
|
|
1371
|
+
return undefined;
|
|
1372
|
+
const scrollFromBottom = Math.max(0, Math.trunc(value.scrollFromBottom));
|
|
1373
|
+
const detachedScrollStart = typeof value.detachedScrollStart === "number" && Number.isFinite(value.detachedScrollStart)
|
|
1374
|
+
? Math.max(0, Math.trunc(value.detachedScrollStart))
|
|
1375
|
+
: undefined;
|
|
1376
|
+
return {
|
|
1377
|
+
scrollFromBottom,
|
|
1378
|
+
...(detachedScrollStart === undefined ? {} : { detachedScrollStart }),
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1309
1381
|
parsePersistedSubmittedUserMessages(value) {
|
|
1310
1382
|
if (!Array.isArray(value))
|
|
1311
1383
|
return [];
|
|
@@ -1341,6 +1413,11 @@ export class AppTabsController {
|
|
|
1341
1413
|
continue;
|
|
1342
1414
|
seen.add(sessionPath);
|
|
1343
1415
|
const persistedTab = { path: sessionPath, title: tab.title };
|
|
1416
|
+
const scrollState = tab.id === this.activeTabId
|
|
1417
|
+
? this.host.captureSessionView?.().scrollState
|
|
1418
|
+
: this.sessionViewsByTabId.get(tab.id)?.scrollState ?? this.scrollStatesByTabId.get(tab.id);
|
|
1419
|
+
if (scrollState)
|
|
1420
|
+
persistedTab.scrollState = scrollState;
|
|
1344
1421
|
const input = this.inputStatesByTabId.get(tab.id);
|
|
1345
1422
|
if (input && (input.text.length > 0 || (input.attachments?.length ?? 0) > 0)) {
|
|
1346
1423
|
persistedTab.input = {
|
|
@@ -1349,6 +1426,10 @@ export class AppTabsController {
|
|
|
1349
1426
|
...(input.attachments && input.attachments.length > 0 ? { attachments: input.attachments.map(clonePersistedAttachment) } : {}),
|
|
1350
1427
|
};
|
|
1351
1428
|
}
|
|
1429
|
+
const autoUserMessages = this.autoUserMessagesByTabId.get(tab.id);
|
|
1430
|
+
if (autoUserMessages && autoUserMessages.length > 0) {
|
|
1431
|
+
persistedTab.autoUserMessages = autoUserMessages.map((message) => this.cloneSubmittedUserMessage(message));
|
|
1432
|
+
}
|
|
1352
1433
|
const deferredUserMessages = this.deferredUserMessagesByTabId.get(tab.id);
|
|
1353
1434
|
if (deferredUserMessages && deferredUserMessages.length > 0) {
|
|
1354
1435
|
persistedTab.deferredUserMessages = deferredUserMessages.map((message) => this.cloneSubmittedUserMessage(message));
|
package/dist/app/types.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type SessionActivity = "idle" | "running" | "thinking";
|
|
|
12
12
|
export type SessionTabStatus = "active" | "waiting";
|
|
13
13
|
export type SessionTabAttention = "terminal-bell";
|
|
14
14
|
export type QueuedMessageMode = "steering" | "follow-up";
|
|
15
|
-
export type QueuedMessageSource = "sdk-steering" | "sdk-follow-up" | "deferred";
|
|
15
|
+
export type QueuedMessageSource = "sdk-steering" | "sdk-follow-up" | "auto" | "deferred";
|
|
16
16
|
export type SubmittedUserMessage = {
|
|
17
17
|
id: string;
|
|
18
18
|
promptText: string;
|
|
@@ -73,6 +73,7 @@ export type Entry = {
|
|
|
73
73
|
text: string;
|
|
74
74
|
expanded: boolean;
|
|
75
75
|
status: "running" | "done";
|
|
76
|
+
level?: string;
|
|
76
77
|
} | {
|
|
77
78
|
id: string;
|
|
78
79
|
kind: "tool";
|
|
@@ -265,6 +266,8 @@ export type StatusLineLayout = {
|
|
|
265
266
|
inputBorderWidgetStartColumn?: number;
|
|
266
267
|
modelUsageLabel?: string;
|
|
267
268
|
contextBarLabel?: string;
|
|
269
|
+
quickScrollUpWidget?: StatusQuickScrollWidgetLayout;
|
|
270
|
+
quickScrollDownWidget?: StatusQuickScrollWidgetLayout;
|
|
268
271
|
userJumpWidget?: StatusUserJumpWidgetLayout;
|
|
269
272
|
draftQueueWidget?: StatusDraftQueueWidgetLayout;
|
|
270
273
|
thinkingExpandWidget?: StatusThinkingExpandWidgetLayout;
|
|
@@ -273,6 +276,10 @@ export type StatusLineLayout = {
|
|
|
273
276
|
promptEnhancerWidget?: StatusPromptEnhancerWidgetLayout;
|
|
274
277
|
voiceWidget?: StatusVoiceWidgetLayout;
|
|
275
278
|
};
|
|
279
|
+
export type StatusQuickScrollWidgetLayout = {
|
|
280
|
+
startColumn: number;
|
|
281
|
+
endColumn: number;
|
|
282
|
+
};
|
|
276
283
|
export type StatusUserJumpWidgetLayout = {
|
|
277
284
|
startColumn: number;
|
|
278
285
|
endColumn: number;
|
|
@@ -587,6 +594,12 @@ export type StatusCompactToolsTarget = {
|
|
|
587
594
|
startColumn: number;
|
|
588
595
|
endColumn: number;
|
|
589
596
|
};
|
|
597
|
+
export type StatusQuickScrollTarget = {
|
|
598
|
+
row: number;
|
|
599
|
+
startColumn: number;
|
|
600
|
+
endColumn: number;
|
|
601
|
+
direction: "up" | "down";
|
|
602
|
+
};
|
|
590
603
|
export type ParsedSlashInput = {
|
|
591
604
|
commandName: string;
|
|
592
605
|
hasArguments: boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import type { NormalizedQuestion, QuestionToolInput } from "./types.js";
|
|
3
|
+
export declare const CUSTOM_ANSWER_LABEL = "Something else\u2026";
|
|
4
|
+
export declare const CUSTOM_ANSWER_SENTINEL_VALUE = "__question_custom_answer__";
|
|
5
|
+
export declare const MIN_QUESTIONS = 1;
|
|
6
|
+
export declare const MAX_QUESTIONS = 5;
|
|
7
|
+
export declare const MIN_CHOICES = 2;
|
|
8
|
+
export declare const MAX_CHOICES = 5;
|
|
9
|
+
export declare const QUESTION_ID_PATTERN: RegExp;
|
|
10
|
+
export declare const questionParameters: Type.TObject<{
|
|
11
|
+
questions: Type.TArray<Type.TObject<{
|
|
12
|
+
id: Type.TString;
|
|
13
|
+
label: Type.TString;
|
|
14
|
+
prompt: Type.TString;
|
|
15
|
+
choices: Type.TArray<Type.TObject<{
|
|
16
|
+
value: Type.TString;
|
|
17
|
+
label: Type.TString;
|
|
18
|
+
description: Type.TOptional<Type.TString>;
|
|
19
|
+
}>>;
|
|
20
|
+
}>>;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function normalizeQuestionInput(input: QuestionToolInput): NormalizedQuestion[];
|
|
23
|
+
export declare function trimString(value: string, field: string): string;
|
|
24
|
+
export declare function normalizeForUniqueness(value: string): string;
|
|
25
|
+
export declare function throwInvalid(problem: string, repair: string): never;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
export const CUSTOM_ANSWER_LABEL = "Something else…";
|
|
3
|
+
export const CUSTOM_ANSWER_SENTINEL_VALUE = "__question_custom_answer__";
|
|
4
|
+
export const MIN_QUESTIONS = 1;
|
|
5
|
+
export const MAX_QUESTIONS = 5;
|
|
6
|
+
export const MIN_CHOICES = 2;
|
|
7
|
+
export const MAX_CHOICES = 5;
|
|
8
|
+
export const QUESTION_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
9
|
+
export const questionParameters = Type.Object({
|
|
10
|
+
questions: Type.Array(Type.Object({
|
|
11
|
+
id: Type.String({ description: "Unique stable question id." }),
|
|
12
|
+
label: Type.String({ description: "Short label for the question." }),
|
|
13
|
+
prompt: Type.String({ description: "Full question prompt to show the user." }),
|
|
14
|
+
choices: Type.Array(Type.Object({
|
|
15
|
+
value: Type.String({ description: "Value returned if this choice is selected." }),
|
|
16
|
+
label: Type.String({ description: "Display label for this choice." }),
|
|
17
|
+
description: Type.Optional(Type.String({ description: "Optional supporting text for this choice." })),
|
|
18
|
+
}), {
|
|
19
|
+
minItems: MIN_CHOICES,
|
|
20
|
+
maxItems: MAX_CHOICES,
|
|
21
|
+
description: "Two to five meaningful predefined choices.",
|
|
22
|
+
}),
|
|
23
|
+
}), {
|
|
24
|
+
minItems: MIN_QUESTIONS,
|
|
25
|
+
maxItems: MAX_QUESTIONS,
|
|
26
|
+
description: "One to five questions to ask the user.",
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
export function normalizeQuestionInput(input) {
|
|
30
|
+
const questions = input.questions;
|
|
31
|
+
if (!Array.isArray(questions) || questions.length < MIN_QUESTIONS || questions.length > MAX_QUESTIONS) {
|
|
32
|
+
throwInvalid(`question requires ${MIN_QUESTIONS} to ${MAX_QUESTIONS} questions; received ${Array.isArray(questions) ? questions.length : "no"}.`, "Retry with a questions array containing one to five questions.");
|
|
33
|
+
}
|
|
34
|
+
const seenQuestionIds = new Set();
|
|
35
|
+
const seenQuestionLabels = new Set();
|
|
36
|
+
return questions.map((question, questionIndex) => {
|
|
37
|
+
const questionNumber = questionIndex + 1;
|
|
38
|
+
const id = trimString(question.id, `question ${questionNumber} id`);
|
|
39
|
+
const label = trimString(question.label, `question ${questionNumber} label`);
|
|
40
|
+
const prompt = trimString(question.prompt, `question ${questionNumber} prompt`);
|
|
41
|
+
if (!QUESTION_ID_PATTERN.test(id))
|
|
42
|
+
throwInvalid(`Question ${questionNumber} has invalid id "${id}". IDs must match ${QUESTION_ID_PATTERN.source}.`, "Retry with a stable id starting with a lowercase letter and containing only lowercase letters, numbers, underscores, or hyphens.");
|
|
43
|
+
if (seenQuestionIds.has(id))
|
|
44
|
+
throwInvalid(`Duplicate question id "${id}" makes question answers ambiguous.`, "Retry with a unique stable id for each question.");
|
|
45
|
+
seenQuestionIds.add(id);
|
|
46
|
+
const normalizedLabel = normalizeForUniqueness(label);
|
|
47
|
+
if (seenQuestionLabels.has(normalizedLabel))
|
|
48
|
+
throwInvalid(`Duplicate question label "${label}" makes question summaries ambiguous.`, "Retry with a unique short label for each question; labels are compared case-insensitively.");
|
|
49
|
+
seenQuestionLabels.add(normalizedLabel);
|
|
50
|
+
if (label.includes("\n") || label.includes("\r"))
|
|
51
|
+
throwInvalid(`Question ${questionNumber} label must be single-line.`, "Retry with a short single-line label and put longer text in the prompt.");
|
|
52
|
+
if (!Array.isArray(question.choices) || question.choices.length < MIN_CHOICES || question.choices.length > MAX_CHOICES) {
|
|
53
|
+
throwInvalid(`Question "${id}" requires ${MIN_CHOICES} to ${MAX_CHOICES} predefined Choices; received ${Array.isArray(question.choices) ? question.choices.length : "no"}.`, "Retry with two to five meaningful predefined Choices for each question.");
|
|
54
|
+
}
|
|
55
|
+
const seenChoiceValues = new Set();
|
|
56
|
+
const seenChoiceLabels = new Set();
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
label,
|
|
60
|
+
prompt,
|
|
61
|
+
choices: question.choices.map((choice, choiceIndex) => {
|
|
62
|
+
const choiceNumber = choiceIndex + 1;
|
|
63
|
+
const value = trimString(choice.value, `question "${id}" Choice ${choiceNumber} value`);
|
|
64
|
+
const choiceLabel = trimString(choice.label, `question "${id}" Choice ${choiceNumber} label`);
|
|
65
|
+
const description = choice.description === undefined ? undefined : choice.description.trim();
|
|
66
|
+
if (value === CUSTOM_ANSWER_SENTINEL_VALUE)
|
|
67
|
+
throwInvalid(`Question "${id}" Choice ${choiceNumber} uses the reserved Custom Answer sentinel value "${CUSTOM_ANSWER_SENTINEL_VALUE}".`, "Retry with a different Choice value; ordinary values like other or custom are allowed.");
|
|
68
|
+
const normalizedValue = normalizeForUniqueness(value);
|
|
69
|
+
if (seenChoiceValues.has(normalizedValue))
|
|
70
|
+
throwInvalid(`Question "${id}" has duplicate Choice value "${value}".`, "Retry with unique Choice values within each question; values are compared case-insensitively.");
|
|
71
|
+
seenChoiceValues.add(normalizedValue);
|
|
72
|
+
const normalizedChoiceLabel = normalizeForUniqueness(choiceLabel);
|
|
73
|
+
if (seenChoiceLabels.has(normalizedChoiceLabel))
|
|
74
|
+
throwInvalid(`Question "${id}" has duplicate Choice label "${choiceLabel}".`, "Retry with unique visible Choice labels within each question; labels are compared case-insensitively.");
|
|
75
|
+
seenChoiceLabels.add(normalizedChoiceLabel);
|
|
76
|
+
if (normalizedChoiceLabel === normalizeForUniqueness(CUSTOM_ANSWER_LABEL))
|
|
77
|
+
throwInvalid(`Question "${id}" Choice label "${choiceLabel}" collides with the implicit Custom Answer row label "${CUSTOM_ANSWER_LABEL}".`, "Retry with a different predefined Choice label; the Custom Answer row is added automatically.");
|
|
78
|
+
return description === undefined ? { value, label: choiceLabel } : { value, label: choiceLabel, description };
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export function trimString(value, field) {
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
if (!trimmed)
|
|
86
|
+
throwInvalid(`${field} must not be empty after trimming.`, "Retry with non-empty text for every required question field.");
|
|
87
|
+
return trimmed;
|
|
88
|
+
}
|
|
89
|
+
export function normalizeForUniqueness(value) {
|
|
90
|
+
return value.toLocaleLowerCase();
|
|
91
|
+
}
|
|
92
|
+
export function throwInvalid(problem, repair) {
|
|
93
|
+
throw new Error(`Invalid question input: ${problem} ${repair}`);
|
|
94
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface ExtensionApiLike {
|
|
2
|
+
registerTool(tool: unknown): void;
|
|
3
|
+
}
|
|
4
|
+
export default function questionExtension(pi: ExtensionApiLike): void;
|
|
5
|
+
export { questionParameters, normalizeQuestionInput } from "./contract.js";
|
|
6
|
+
export { createCanceledQuestionResult, createFallbackPrompt, createQuestionToolResult, createSuccessfulQuestionResult, summarizeQuestionResult } from "./result.js";
|
|
7
|
+
export type { QuestionResultDetails, QuestionToolInput } from "./types.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { QUESTION_TOOL_DESCRIPTION } from "./tool-description.js";
|
|
2
|
+
import { questionParameters, normalizeQuestionInput } from "./contract.js";
|
|
3
|
+
import { renderQuestionCall, renderQuestionResult } from "./render.js";
|
|
4
|
+
import { createCanceledQuestionResult, createQuestionToolResult, createSuccessfulQuestionResult } from "./result.js";
|
|
5
|
+
import { runQuestionnaire } from "./tui.js";
|
|
6
|
+
export default function questionExtension(pi) {
|
|
7
|
+
pi.registerTool({
|
|
8
|
+
...QUESTION_TOOL_DESCRIPTION,
|
|
9
|
+
parameters: questionParameters,
|
|
10
|
+
renderCall(args, theme) {
|
|
11
|
+
return renderQuestionCall(args, theme);
|
|
12
|
+
},
|
|
13
|
+
renderResult(result, _options, theme, context) {
|
|
14
|
+
return renderQuestionResult(result, theme, context.args);
|
|
15
|
+
},
|
|
16
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
17
|
+
const questions = normalizeQuestionInput(params);
|
|
18
|
+
if (!ctx.hasUI)
|
|
19
|
+
return createQuestionToolResult(createCanceledQuestionResult("ui_unavailable", questions), questions);
|
|
20
|
+
const selections = await runQuestionnaire(questions, ctx);
|
|
21
|
+
if (selections === null)
|
|
22
|
+
return createQuestionToolResult(createCanceledQuestionResult("user_canceled"), questions);
|
|
23
|
+
return createQuestionToolResult(createSuccessfulQuestionResult(questions, selections), questions);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export { questionParameters, normalizeQuestionInput } from "./contract.js";
|
|
28
|
+
export { createCanceledQuestionResult, createFallbackPrompt, createQuestionToolResult, createSuccessfulQuestionResult, summarizeQuestionResult } from "./result.js";
|