@gajae-code/coding-agent 0.7.9 → 0.7.10
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/CHANGELOG.md +22 -0
- package/README.md +2 -0
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +20 -6
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +2 -0
- package/dist/types/utils/cmux-workspace.d.ts +23 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +5 -0
- package/dist/types/web/search/render.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/web-search-cli.ts +3 -2
- package/src/config/settings-schema.ts +11 -1
- package/src/coordinator-mcp/server.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +26 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/gjc-runtime/launch-tmux.ts +63 -2
- package/src/gjc-runtime/ultragoal-guard.ts +139 -19
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
- package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
- package/src/gjc-runtime/workflow-manifest.ts +0 -1
- package/src/hooks/skill-state.ts +33 -73
- package/src/internal-urls/docs-index.generated.ts +2 -1
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/settings-selector.ts +47 -3
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/controllers/extension-ui-controller.ts +18 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +53 -9
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +12 -1
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +67 -24
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/sdk.ts +3 -5
- package/src/tools/ask.ts +136 -16
- package/src/utils/cmux-workspace.ts +106 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/search/index.ts +22 -10
- package/src/web/search/provider.ts +15 -0
- package/src/web/search/render.ts +3 -0
package/src/tools/ask.ts
CHANGED
|
@@ -91,6 +91,7 @@ export interface QuestionResult {
|
|
|
91
91
|
multi: boolean;
|
|
92
92
|
selectedOptions: string[];
|
|
93
93
|
customInput?: string;
|
|
94
|
+
clarificationQuestion?: string;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
export interface AskToolDetails {
|
|
@@ -99,6 +100,7 @@ export interface AskToolDetails {
|
|
|
99
100
|
multi?: boolean;
|
|
100
101
|
selectedOptions?: string[];
|
|
101
102
|
customInput?: string;
|
|
103
|
+
clarificationQuestion?: string;
|
|
102
104
|
/** Multi-part question mode */
|
|
103
105
|
results?: QuestionResult[];
|
|
104
106
|
}
|
|
@@ -108,6 +110,7 @@ export interface AskToolDetails {
|
|
|
108
110
|
// =============================================================================
|
|
109
111
|
|
|
110
112
|
const OTHER_OPTION = "Other (type your own)";
|
|
113
|
+
const ASK_CLARIFICATION_OPTION = "Ask about these choices";
|
|
111
114
|
const RECOMMENDED_SUFFIX = " (Recommended)";
|
|
112
115
|
const DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS = Number.MAX_SAFE_INTEGER;
|
|
113
116
|
const DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS = 250;
|
|
@@ -183,6 +186,7 @@ function numberOptionLabels(labels: string[]): string[] {
|
|
|
183
186
|
interface SelectionResult {
|
|
184
187
|
selectedOptions: string[];
|
|
185
188
|
customInput?: string;
|
|
189
|
+
clarificationQuestion?: string;
|
|
186
190
|
timedOut: boolean;
|
|
187
191
|
navigation?: "back" | "forward";
|
|
188
192
|
cancelled?: boolean;
|
|
@@ -201,6 +205,7 @@ interface AskSingleQuestionOptions {
|
|
|
201
205
|
navigation?: NavigationControls;
|
|
202
206
|
scrollTitleRows?: number;
|
|
203
207
|
otherOptionLabel?: string;
|
|
208
|
+
clarificationOptionLabel?: string;
|
|
204
209
|
}
|
|
205
210
|
|
|
206
211
|
interface UIContext {
|
|
@@ -219,6 +224,7 @@ interface UIContext {
|
|
|
219
224
|
onRight?: () => void;
|
|
220
225
|
helpText?: string;
|
|
221
226
|
customInput?: { optionLabel: string; onSubmit: (text: string) => void };
|
|
227
|
+
clarificationInput?: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean };
|
|
222
228
|
},
|
|
223
229
|
): Promise<string | undefined>;
|
|
224
230
|
editor(
|
|
@@ -239,6 +245,7 @@ async function askSingleQuestion(
|
|
|
239
245
|
const { recommended, timeout, signal, initialSelection, navigation, scrollTitleRows } = options;
|
|
240
246
|
const doneLabel = getDoneOptionLabel();
|
|
241
247
|
const otherOptionLabel = options.otherOptionLabel ?? OTHER_OPTION;
|
|
248
|
+
const clarificationOptionLabel = options.clarificationOptionLabel;
|
|
242
249
|
let selectedOptions = [...(initialSelection?.selectedOptions ?? [])];
|
|
243
250
|
let customInput = initialSelection?.customInput;
|
|
244
251
|
let timedOut = false;
|
|
@@ -252,15 +259,17 @@ async function askSingleQuestion(
|
|
|
252
259
|
timedOut: boolean;
|
|
253
260
|
navigation?: "back" | "forward";
|
|
254
261
|
inlineInput?: string;
|
|
262
|
+
inlineClarification?: string;
|
|
255
263
|
}> => {
|
|
256
264
|
let timeoutTriggered = false;
|
|
257
265
|
const onTimeout = () => {
|
|
258
266
|
timeoutTriggered = true;
|
|
259
267
|
};
|
|
260
|
-
// Inline custom input: the TUI selector keeps the question
|
|
261
|
-
// list on screen and collects
|
|
262
|
-
//
|
|
268
|
+
// Inline custom/clarification input: the TUI selector keeps the question
|
|
269
|
+
// and option list on screen and collects text below the list, instead of
|
|
270
|
+
// swapping to a separate editor screen that hides the question.
|
|
263
271
|
let inlineInput: string | undefined;
|
|
272
|
+
let inlineClarification: string | undefined;
|
|
264
273
|
let navigationAction: "back" | "forward" | undefined;
|
|
265
274
|
const baseHelpText = navigation
|
|
266
275
|
? "up/down navigate enter select ←/→ question esc cancel"
|
|
@@ -286,6 +295,15 @@ async function askSingleQuestion(
|
|
|
286
295
|
inlineInput = text;
|
|
287
296
|
},
|
|
288
297
|
},
|
|
298
|
+
clarificationInput: clarificationOptionLabel
|
|
299
|
+
? {
|
|
300
|
+
optionLabel: clarificationOptionLabel,
|
|
301
|
+
allowEmpty: false,
|
|
302
|
+
onSubmit: (text: string) => {
|
|
303
|
+
inlineClarification = text;
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
: undefined,
|
|
289
307
|
onLeft: navigation?.allowBack
|
|
290
308
|
? () => {
|
|
291
309
|
navigationAction = "back";
|
|
@@ -304,7 +322,7 @@ async function askSingleQuestion(
|
|
|
304
322
|
if (!timeoutTriggered && choice === undefined && typeof timeout === "number") {
|
|
305
323
|
timeoutTriggered = Date.now() - startMs >= timeout;
|
|
306
324
|
}
|
|
307
|
-
return { choice, timedOut: timeoutTriggered, navigation: navigationAction, inlineInput };
|
|
325
|
+
return { choice, timedOut: timeoutTriggered, navigation: navigationAction, inlineInput, inlineClarification };
|
|
308
326
|
};
|
|
309
327
|
|
|
310
328
|
// Fallback for UI contexts that don't support inline custom input (they
|
|
@@ -315,6 +333,13 @@ async function askSingleQuestion(
|
|
|
315
333
|
const input = signal ? await untilAborted(signal, showCustomInput) : await showCustomInput();
|
|
316
334
|
return { input };
|
|
317
335
|
};
|
|
336
|
+
const promptForClarificationInput = async (): Promise<{ input: string | undefined }> => {
|
|
337
|
+
const dialogOptions = signal ? { signal } : undefined;
|
|
338
|
+
const showClarificationInput = () =>
|
|
339
|
+
ui.editor("Ask a clarification question:", undefined, dialogOptions, { promptStyle: true });
|
|
340
|
+
const input = signal ? await untilAborted(signal, showClarificationInput) : await showClarificationInput();
|
|
341
|
+
return { input: input !== undefined && input.trim() === "" ? undefined : input };
|
|
342
|
+
};
|
|
318
343
|
|
|
319
344
|
const promptWithProgress = navigation?.progressText ? `${question} (${navigation.progressText})` : question;
|
|
320
345
|
if (multi) {
|
|
@@ -337,6 +362,9 @@ async function askSingleQuestion(
|
|
|
337
362
|
opts.push(doneLabel);
|
|
338
363
|
}
|
|
339
364
|
opts.push(otherOptionLabel);
|
|
365
|
+
if (clarificationOptionLabel) {
|
|
366
|
+
opts.push(clarificationOptionLabel);
|
|
367
|
+
}
|
|
340
368
|
|
|
341
369
|
const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
|
|
342
370
|
const {
|
|
@@ -344,6 +372,7 @@ async function askSingleQuestion(
|
|
|
344
372
|
timedOut: selectTimedOut,
|
|
345
373
|
navigation: arrowNavigation,
|
|
346
374
|
inlineInput,
|
|
375
|
+
inlineClarification,
|
|
347
376
|
} = await selectOption(`${prefix}${promptWithProgress}`, opts, cursorIndex);
|
|
348
377
|
|
|
349
378
|
if (arrowNavigation) {
|
|
@@ -370,6 +399,18 @@ async function askSingleQuestion(
|
|
|
370
399
|
customInput = input;
|
|
371
400
|
break;
|
|
372
401
|
}
|
|
402
|
+
if (clarificationOptionLabel && choice === clarificationOptionLabel) {
|
|
403
|
+
if (selectTimedOut) {
|
|
404
|
+
timedOut = true;
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
const input =
|
|
408
|
+
inlineClarification !== undefined ? inlineClarification : (await promptForClarificationInput()).input;
|
|
409
|
+
if (input === undefined) {
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
return { selectedOptions: [], clarificationQuestion: input, timedOut };
|
|
413
|
+
}
|
|
373
414
|
|
|
374
415
|
const selectedIdx = opts.indexOf(choice);
|
|
375
416
|
if (selectedIdx >= 0) {
|
|
@@ -400,7 +441,9 @@ async function askSingleQuestion(
|
|
|
400
441
|
selectedOptions = Array.from(selected);
|
|
401
442
|
} else {
|
|
402
443
|
const displayLabels = addRecommendedSuffix(optionLabels, recommended);
|
|
403
|
-
const optionsWithNavigation =
|
|
444
|
+
const optionsWithNavigation = clarificationOptionLabel
|
|
445
|
+
? [...displayLabels, otherOptionLabel, clarificationOptionLabel]
|
|
446
|
+
: [...displayLabels, otherOptionLabel];
|
|
404
447
|
|
|
405
448
|
let initialIndex = recommended;
|
|
406
449
|
const previouslySelected = selectedOptions[0];
|
|
@@ -420,6 +463,7 @@ async function askSingleQuestion(
|
|
|
420
463
|
timedOut: selectTimedOut,
|
|
421
464
|
navigation: arrowNavigation,
|
|
422
465
|
inlineInput,
|
|
466
|
+
inlineClarification,
|
|
423
467
|
} = await selectOption(promptWithProgress, optionsWithNavigation, initialIndex);
|
|
424
468
|
timedOut = selectTimedOut;
|
|
425
469
|
|
|
@@ -439,6 +483,15 @@ async function askSingleQuestion(
|
|
|
439
483
|
}
|
|
440
484
|
// If input was dismissed (undefined), keep prior selectedOptions/customInput intact
|
|
441
485
|
}
|
|
486
|
+
} else if (clarificationOptionLabel && choice === clarificationOptionLabel) {
|
|
487
|
+
if (!selectTimedOut) {
|
|
488
|
+
const input =
|
|
489
|
+
inlineClarification !== undefined ? inlineClarification : (await promptForClarificationInput()).input;
|
|
490
|
+
if (input !== undefined) {
|
|
491
|
+
return { selectedOptions: [], clarificationQuestion: input, timedOut };
|
|
492
|
+
}
|
|
493
|
+
// If input was dismissed (undefined), keep prior selectedOptions/customInput intact
|
|
494
|
+
}
|
|
442
495
|
} else {
|
|
443
496
|
const stripped = stripRecommendedSuffix(choice);
|
|
444
497
|
if (optionLabels.includes(stripped)) {
|
|
@@ -466,6 +519,9 @@ async function askSingleQuestion(
|
|
|
466
519
|
}
|
|
467
520
|
|
|
468
521
|
function formatQuestionResult(result: QuestionResult): string {
|
|
522
|
+
if (result.clarificationQuestion !== undefined) {
|
|
523
|
+
return `${result.id}: clarification requested: ${result.clarificationQuestion}`;
|
|
524
|
+
}
|
|
469
525
|
if (result.customInput !== undefined) {
|
|
470
526
|
return `${result.id}: "${result.customInput}"`;
|
|
471
527
|
}
|
|
@@ -682,6 +738,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
682
738
|
optionLabels: rawOptionLabels,
|
|
683
739
|
selectedOptions: decoded.selectedOptions,
|
|
684
740
|
customInput: decoded.customInput,
|
|
741
|
+
clarificationQuestion: decoded.clarificationQuestion,
|
|
685
742
|
navigation: undefined as NavigationControls | undefined,
|
|
686
743
|
cancelled: false,
|
|
687
744
|
timedOut: false,
|
|
@@ -693,6 +750,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
693
750
|
const displayQuestion = deepInterviewPrompt ?? q.question;
|
|
694
751
|
const shouldNumberOptions = isDeepInterviewQuestion || isDeepInterviewAskQuestion(q.question);
|
|
695
752
|
const optionLabels = shouldNumberOptions ? numberOptionLabels(rawOptionLabels) : rawOptionLabels;
|
|
753
|
+
const clarificationOptionLabel = shouldNumberOptions
|
|
754
|
+
? formatNumberedOptionLabel(ASK_CLARIFICATION_OPTION, optionLabels.length + 1)
|
|
755
|
+
: undefined;
|
|
696
756
|
const initialSelection =
|
|
697
757
|
shouldNumberOptions && options?.previous
|
|
698
758
|
? {
|
|
@@ -706,6 +766,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
706
766
|
const {
|
|
707
767
|
selectedOptions: displaySelectedOptions,
|
|
708
768
|
customInput,
|
|
769
|
+
clarificationQuestion,
|
|
709
770
|
navigation,
|
|
710
771
|
cancelled,
|
|
711
772
|
timedOut,
|
|
@@ -719,6 +780,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
719
780
|
otherOptionLabel: shouldNumberOptions
|
|
720
781
|
? formatNumberedOptionLabel(OTHER_OPTION, optionLabels.length)
|
|
721
782
|
: undefined,
|
|
783
|
+
clarificationOptionLabel,
|
|
722
784
|
});
|
|
723
785
|
const selectedOptions = shouldNumberOptions
|
|
724
786
|
? displaySelectedOptions.map(selected => {
|
|
@@ -726,7 +788,15 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
726
788
|
return displayIndex >= 0 ? (rawOptionLabels[displayIndex] ?? selected) : selected;
|
|
727
789
|
})
|
|
728
790
|
: displaySelectedOptions;
|
|
729
|
-
return {
|
|
791
|
+
return {
|
|
792
|
+
optionLabels: rawOptionLabels,
|
|
793
|
+
selectedOptions,
|
|
794
|
+
customInput,
|
|
795
|
+
clarificationQuestion,
|
|
796
|
+
navigation,
|
|
797
|
+
cancelled,
|
|
798
|
+
timedOut,
|
|
799
|
+
};
|
|
730
800
|
} catch (error) {
|
|
731
801
|
if (error instanceof Error && error.name === "AbortError") {
|
|
732
802
|
throw new ToolAbortError("Ask input was cancelled");
|
|
@@ -737,22 +807,40 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
737
807
|
|
|
738
808
|
if (params.questions.length === 1) {
|
|
739
809
|
const [q] = params.questions;
|
|
740
|
-
const { optionLabels, selectedOptions, customInput, cancelled, timedOut } =
|
|
741
|
-
|
|
742
|
-
|
|
810
|
+
const { optionLabels, selectedOptions, customInput, clarificationQuestion, cancelled, timedOut } =
|
|
811
|
+
await askQuestion(q);
|
|
812
|
+
|
|
813
|
+
if (
|
|
814
|
+
!timedOut &&
|
|
815
|
+
(cancelled ||
|
|
816
|
+
(selectedOptions.length === 0 && customInput === undefined && clarificationQuestion === undefined))
|
|
817
|
+
) {
|
|
743
818
|
context?.abort();
|
|
744
819
|
throw new ToolAbortError("Ask tool was cancelled by the user");
|
|
745
820
|
}
|
|
746
|
-
|
|
821
|
+
if (clarificationQuestion === undefined) {
|
|
822
|
+
await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
|
|
823
|
+
}
|
|
747
824
|
const details: AskToolDetails = {
|
|
748
825
|
question: q.question,
|
|
749
826
|
options: optionLabels,
|
|
750
827
|
multi: q.multi ?? false,
|
|
751
828
|
selectedOptions,
|
|
752
829
|
customInput,
|
|
830
|
+
clarificationQuestion,
|
|
753
831
|
};
|
|
754
832
|
|
|
755
833
|
const responseParts: string[] = [];
|
|
834
|
+
if (clarificationQuestion !== undefined) {
|
|
835
|
+
responseParts.push(
|
|
836
|
+
clarificationQuestion.includes("\n")
|
|
837
|
+
? `User asked a clarification question about the choices:\n${clarificationQuestion
|
|
838
|
+
.split("\n")
|
|
839
|
+
.map(line => ` ${line}`)
|
|
840
|
+
.join("\n")}`
|
|
841
|
+
: `User asked a clarification question about the choices: ${clarificationQuestion}`,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
756
844
|
if (selectedOptions.length > 0) {
|
|
757
845
|
responseParts.push(
|
|
758
846
|
q.multi ? `User selected: ${selectedOptions.join(", ")}` : `User selected: ${selectedOptions[0]}`,
|
|
@@ -787,6 +875,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
787
875
|
optionLabels,
|
|
788
876
|
selectedOptions,
|
|
789
877
|
customInput,
|
|
878
|
+
clarificationQuestion,
|
|
790
879
|
navigation: navAction,
|
|
791
880
|
cancelled,
|
|
792
881
|
timedOut,
|
|
@@ -804,9 +893,12 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
804
893
|
multi: q.multi ?? false,
|
|
805
894
|
selectedOptions,
|
|
806
895
|
customInput,
|
|
896
|
+
clarificationQuestion,
|
|
807
897
|
};
|
|
808
898
|
|
|
809
|
-
|
|
899
|
+
if (clarificationQuestion === undefined) {
|
|
900
|
+
await this.#recordDeepInterviewRound(q, selectedOptions, customInput);
|
|
901
|
+
}
|
|
810
902
|
|
|
811
903
|
if (navAction === "back") {
|
|
812
904
|
questionIndex = Math.max(0, questionIndex - 1);
|
|
@@ -995,7 +1087,10 @@ export const askToolRenderer = {
|
|
|
995
1087
|
// Multi-part results
|
|
996
1088
|
if (details.results && details.results.length > 0) {
|
|
997
1089
|
const hasAnySelection = details.results.some(
|
|
998
|
-
r =>
|
|
1090
|
+
r =>
|
|
1091
|
+
r.clarificationQuestion !== undefined ||
|
|
1092
|
+
r.customInput !== undefined ||
|
|
1093
|
+
(r.selectedOptions && r.selectedOptions.length > 0),
|
|
999
1094
|
);
|
|
1000
1095
|
const header = renderStatusLine(
|
|
1001
1096
|
{
|
|
@@ -1013,7 +1108,8 @@ export const askToolRenderer = {
|
|
|
1013
1108
|
const isLastQuestion = i === details.results.length - 1;
|
|
1014
1109
|
const branch = isLastQuestion ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
1015
1110
|
const continuation = isLastQuestion ? " " : `${uiTheme.fg("dim", uiTheme.tree.vertical)} `;
|
|
1016
|
-
const hasSelection =
|
|
1111
|
+
const hasSelection =
|
|
1112
|
+
r.clarificationQuestion !== undefined || r.customInput !== undefined || r.selectedOptions.length > 0;
|
|
1017
1113
|
const statusIcon = hasSelection
|
|
1018
1114
|
? uiTheme.styledSymbol("status.success", "success")
|
|
1019
1115
|
: uiTheme.styledSymbol("status.warning", "warning");
|
|
@@ -1028,7 +1124,10 @@ export const askToolRenderer = {
|
|
|
1028
1124
|
|
|
1029
1125
|
const answerLines: string[] = [];
|
|
1030
1126
|
for (let j = 0; j < r.selectedOptions.length; j++) {
|
|
1031
|
-
const isLast =
|
|
1127
|
+
const isLast =
|
|
1128
|
+
j === r.selectedOptions.length - 1 &&
|
|
1129
|
+
r.customInput === undefined &&
|
|
1130
|
+
r.clarificationQuestion === undefined;
|
|
1032
1131
|
const optBranch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
1033
1132
|
const selectedLabel = renderInlineMarkdown(r.selectedOptions[j], mdTheme, t =>
|
|
1034
1133
|
uiTheme.fg("toolOutput", t),
|
|
@@ -1042,6 +1141,14 @@ export const askToolRenderer = {
|
|
|
1042
1141
|
}
|
|
1043
1142
|
if (r.customInput !== undefined) {
|
|
1044
1143
|
container.addChild(new Text(renderCustomInput(uiTheme, continuation, r.customInput, true, false), 0, 0));
|
|
1144
|
+
} else if (r.clarificationQuestion !== undefined) {
|
|
1145
|
+
container.addChild(
|
|
1146
|
+
new Text(
|
|
1147
|
+
renderCustomInput(uiTheme, continuation, `Clarification: ${r.clarificationQuestion}`, true, false),
|
|
1148
|
+
0,
|
|
1149
|
+
0,
|
|
1150
|
+
),
|
|
1151
|
+
);
|
|
1045
1152
|
} else if (r.selectedOptions.length === 0) {
|
|
1046
1153
|
container.addChild(
|
|
1047
1154
|
new Text(
|
|
@@ -1063,7 +1170,9 @@ export const askToolRenderer = {
|
|
|
1063
1170
|
}
|
|
1064
1171
|
|
|
1065
1172
|
const hasSelection =
|
|
1066
|
-
details.
|
|
1173
|
+
details.clarificationQuestion !== undefined ||
|
|
1174
|
+
details.customInput !== undefined ||
|
|
1175
|
+
(details.selectedOptions && details.selectedOptions.length > 0);
|
|
1067
1176
|
const header = renderStatusLine({ icon: hasSelection ? "success" : "warning", title: "Ask" }, uiTheme);
|
|
1068
1177
|
const container = new Container();
|
|
1069
1178
|
container.addChild(new Text(header, 0, 0));
|
|
@@ -1075,7 +1184,10 @@ export const askToolRenderer = {
|
|
|
1075
1184
|
const answerLines: string[] = [];
|
|
1076
1185
|
if (details.selectedOptions && details.selectedOptions.length > 0) {
|
|
1077
1186
|
for (let i = 0; i < details.selectedOptions.length; i++) {
|
|
1078
|
-
const isLast =
|
|
1187
|
+
const isLast =
|
|
1188
|
+
i === details.selectedOptions.length - 1 &&
|
|
1189
|
+
details.customInput === undefined &&
|
|
1190
|
+
details.clarificationQuestion === undefined;
|
|
1079
1191
|
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
1080
1192
|
const selectedLabel = renderInlineMarkdown(details.selectedOptions[i], mdTheme, t =>
|
|
1081
1193
|
uiTheme.fg("toolOutput", t),
|
|
@@ -1090,6 +1202,14 @@ export const askToolRenderer = {
|
|
|
1090
1202
|
}
|
|
1091
1203
|
if (details.customInput !== undefined) {
|
|
1092
1204
|
container.addChild(new Text(renderCustomInput(uiTheme, " ", details.customInput, true, false), 0, 0));
|
|
1205
|
+
} else if (details.clarificationQuestion !== undefined) {
|
|
1206
|
+
container.addChild(
|
|
1207
|
+
new Text(
|
|
1208
|
+
renderCustomInput(uiTheme, " ", `Clarification: ${details.clarificationQuestion}`, true, false),
|
|
1209
|
+
0,
|
|
1210
|
+
0,
|
|
1211
|
+
),
|
|
1212
|
+
);
|
|
1093
1213
|
} else if (!details.selectedOptions || details.selectedOptions.length === 0) {
|
|
1094
1214
|
container.addChild(
|
|
1095
1215
|
new Text(
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { logger } from "@gajae-code/utils";
|
|
2
|
+
|
|
3
|
+
const CMUX_COMMAND = "cmux";
|
|
4
|
+
const CMUX_WORKSPACE_ID_ENV = "CMUX_WORKSPACE_ID";
|
|
5
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
6
|
+
const CMUX_WORKSPACE_RENAME_TIMEOUT_MS = 1500;
|
|
7
|
+
|
|
8
|
+
export interface CmuxWorkspaceRenameCommand {
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CmuxWorkspaceRenameProcess {
|
|
14
|
+
exited: Promise<number>;
|
|
15
|
+
kill(): void;
|
|
16
|
+
unref(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CmuxWorkspaceTitleSyncOptions {
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
isTty?: boolean;
|
|
22
|
+
which?: (command: string) => string | null;
|
|
23
|
+
spawn?: (
|
|
24
|
+
command: string[],
|
|
25
|
+
options: { env: NodeJS.ProcessEnv; stdin: "ignore"; stdout: "ignore"; stderr: "ignore" },
|
|
26
|
+
) => CmuxWorkspaceRenameProcess;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaultSpawn(
|
|
30
|
+
command: string[],
|
|
31
|
+
options: { env: NodeJS.ProcessEnv; stdin: "ignore"; stdout: "ignore"; stderr: "ignore" },
|
|
32
|
+
): CmuxWorkspaceRenameProcess {
|
|
33
|
+
return Bun.spawn(command, options);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sanitizeCmuxWorkspaceTitle(title: string | undefined): string | undefined {
|
|
37
|
+
if (!title) return undefined;
|
|
38
|
+
const sanitized = title.replace(CONTROL_CHARS, " ").replace(/\s+/g, " ").trim();
|
|
39
|
+
return sanitized || undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildCmuxWorkspaceRenameCommand(
|
|
43
|
+
sessionName: string | undefined,
|
|
44
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
45
|
+
): CmuxWorkspaceRenameCommand | null {
|
|
46
|
+
const workspaceId = env[CMUX_WORKSPACE_ID_ENV]?.trim();
|
|
47
|
+
if (!workspaceId) return null;
|
|
48
|
+
|
|
49
|
+
const title = sanitizeCmuxWorkspaceTitle(sessionName);
|
|
50
|
+
if (!title) return null;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
command: CMUX_COMMAND,
|
|
54
|
+
args: ["workspace", "rename", workspaceId, "--title", title],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function syncCmuxWorkspaceTitle(
|
|
59
|
+
sessionName: string | undefined,
|
|
60
|
+
options: CmuxWorkspaceTitleSyncOptions = {},
|
|
61
|
+
): void {
|
|
62
|
+
const isTty = options.isTty ?? process.stdout.isTTY === true;
|
|
63
|
+
if (!isTty) return;
|
|
64
|
+
|
|
65
|
+
const env = options.env ?? process.env;
|
|
66
|
+
const plan = buildCmuxWorkspaceRenameCommand(sessionName, env);
|
|
67
|
+
if (!plan) return;
|
|
68
|
+
|
|
69
|
+
const which = options.which ?? Bun.which;
|
|
70
|
+
let resolvedCommand: string | null;
|
|
71
|
+
try {
|
|
72
|
+
resolvedCommand = which(plan.command);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger.debug("cmux workspace rename command lookup failed", { error: String(error) });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!resolvedCommand) return;
|
|
78
|
+
|
|
79
|
+
const spawn = options.spawn ?? defaultSpawn;
|
|
80
|
+
try {
|
|
81
|
+
const proc = spawn([resolvedCommand, ...plan.args], {
|
|
82
|
+
env,
|
|
83
|
+
stdin: "ignore",
|
|
84
|
+
stdout: "ignore",
|
|
85
|
+
stderr: "ignore",
|
|
86
|
+
});
|
|
87
|
+
proc.unref();
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
try {
|
|
90
|
+
proc.kill();
|
|
91
|
+
} catch {}
|
|
92
|
+
}, CMUX_WORKSPACE_RENAME_TIMEOUT_MS);
|
|
93
|
+
timer.unref?.();
|
|
94
|
+
void proc.exited
|
|
95
|
+
.then(exitCode => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
if (exitCode !== 0) logger.debug("cmux workspace rename exited non-zero", { exitCode });
|
|
98
|
+
})
|
|
99
|
+
.catch(error => {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
logger.debug("cmux workspace rename failed", { error: String(error) });
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.debug("cmux workspace rename failed to start", { error: String(error) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { ModelRegistry } from "../config/model-registry";
|
|
|
9
9
|
import { resolveRoleSelection } from "../config/model-resolver";
|
|
10
10
|
import type { Settings } from "../config/settings";
|
|
11
11
|
import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
|
|
12
|
+
import { syncCmuxWorkspaceTitle } from "./cmux-workspace";
|
|
12
13
|
|
|
13
14
|
const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
|
|
14
15
|
|
|
@@ -218,6 +219,7 @@ export function setTerminalTitle(title: string): void {
|
|
|
218
219
|
|
|
219
220
|
export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: string): void {
|
|
220
221
|
setTerminalTitle(formatSessionTerminalTitle(sessionName, cwd));
|
|
222
|
+
syncCmuxWorkspaceTitle(sessionName);
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
/**
|
package/src/web/search/index.ts
CHANGED
|
@@ -68,6 +68,18 @@ function formatProviderError(error: unknown, provider: SearchProvider): string {
|
|
|
68
68
|
return `Unknown error from ${provider.label}`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function formatProviderFailure(error: unknown, provider: SearchProvider): string {
|
|
72
|
+
if (error instanceof SearchProviderError) return error.message;
|
|
73
|
+
return `${provider.id}: ${formatProviderError(error, provider)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatFallbackWarning(
|
|
77
|
+
failures: Array<{ provider: SearchProvider; error: unknown }>,
|
|
78
|
+
provider: SearchProvider,
|
|
79
|
+
): string {
|
|
80
|
+
return `Web search provider fallback: ${failures.map(f => formatProviderFailure(f.error, f.provider)).join("; ")}; using ${provider.label}.`;
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
/** Truncate text for tool output */
|
|
72
84
|
function truncateText(text: string, maxLen: number): string {
|
|
73
85
|
if (text.length <= maxLen) return text;
|
|
@@ -185,10 +197,11 @@ async function executeSearch(
|
|
|
185
197
|
});
|
|
186
198
|
|
|
187
199
|
const text = formatForLLM(response);
|
|
200
|
+
const warning = failures.length > 0 ? formatFallbackWarning(failures, provider) : undefined;
|
|
188
201
|
|
|
189
202
|
return {
|
|
190
|
-
content: [{ type: "text" as const, text }],
|
|
191
|
-
details: { response },
|
|
203
|
+
content: [{ type: "text" as const, text: warning ? `Warning: ${warning}\n\n${text}` : text }],
|
|
204
|
+
details: { response, ...(warning ? { warning } : {}) },
|
|
192
205
|
};
|
|
193
206
|
} catch (error) {
|
|
194
207
|
// Surface user-initiated cancellation immediately so the session sees
|
|
@@ -207,13 +220,7 @@ async function executeSearch(
|
|
|
207
220
|
: `Unknown error from ${lastProvider.label}`;
|
|
208
221
|
const message =
|
|
209
222
|
providers.length > 1
|
|
210
|
-
? `All web search providers failed: ${failures
|
|
211
|
-
.map(f =>
|
|
212
|
-
f.error instanceof SearchProviderError
|
|
213
|
-
? f.error.message
|
|
214
|
-
: `${f.provider.id}: ${formatProviderError(f.error, f.provider)}`,
|
|
215
|
-
)
|
|
216
|
-
.join("; ")}`
|
|
223
|
+
? `All web search providers failed: ${failures.map(f => formatProviderFailure(f.error, f.provider)).join("; ")}`
|
|
217
224
|
: baseMessage;
|
|
218
225
|
|
|
219
226
|
return {
|
|
@@ -321,7 +328,12 @@ export function getSearchTools(): CustomTool<any, any>[] {
|
|
|
321
328
|
return [webSearchCustomTool];
|
|
322
329
|
}
|
|
323
330
|
|
|
324
|
-
export {
|
|
331
|
+
export {
|
|
332
|
+
getConfiguredSearchProviderPreference,
|
|
333
|
+
getSearchProvider,
|
|
334
|
+
setPreferredSearchProvider,
|
|
335
|
+
setSearchFallbackProviders,
|
|
336
|
+
} from "./provider";
|
|
325
337
|
export { setSearchHardTimeoutMs } from "./providers/utils";
|
|
326
338
|
export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
|
|
327
339
|
export { isConfigurableSearchProviderId, isSearchProviderPreference } from "./types";
|
|
@@ -149,6 +149,21 @@ export function setSearchFallbackProviders(ids: readonly string[]): void {
|
|
|
149
149
|
fallbackProvIds = ids.filter(isConfigurableSearchProviderId);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
export interface SearchProviderPreferenceSource {
|
|
153
|
+
get(path: "providers.webSearch" | "web_search.provider"): SearchProviderId | "auto";
|
|
154
|
+
has(path: "providers.webSearch" | "web_search.provider"): boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function getConfiguredSearchProviderPreference(
|
|
158
|
+
settings: SearchProviderPreferenceSource,
|
|
159
|
+
): SearchProviderId | "auto" {
|
|
160
|
+
const providerPreference = settings.get("providers.webSearch");
|
|
161
|
+
if (settings.has("providers.webSearch") || providerPreference !== "auto") return providerPreference;
|
|
162
|
+
|
|
163
|
+
const legacyProviderPreference = settings.get("web_search.provider");
|
|
164
|
+
return legacyProviderPreference;
|
|
165
|
+
}
|
|
166
|
+
|
|
152
167
|
export interface ResolveProviderChainOptions {
|
|
153
168
|
authStorage: AuthStorage;
|
|
154
169
|
sessionId?: string;
|
package/src/web/search/render.ts
CHANGED
|
@@ -66,6 +66,7 @@ function renderFallbackText(contentText: string, expanded: boolean, theme: Theme
|
|
|
66
66
|
export interface SearchRenderDetails {
|
|
67
67
|
response: SearchResponse;
|
|
68
68
|
error?: string;
|
|
69
|
+
warning?: string;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/** Render web search result with tree-based layout */
|
|
@@ -130,6 +131,8 @@ export function renderSearchResult(
|
|
|
130
131
|
|
|
131
132
|
const metaLines: string[] = [];
|
|
132
133
|
metaLines.push(`${theme.fg("muted", "Provider:")} ${theme.fg("text", providerLabel)}`);
|
|
134
|
+
if (details.warning)
|
|
135
|
+
metaLines.push(`${theme.fg("muted", "Fallback:")} ${theme.fg("warning", truncateToWidth(details.warning, 110))}`);
|
|
133
136
|
if (response.authMode)
|
|
134
137
|
metaLines.push(
|
|
135
138
|
`${theme.fg("muted", "Auth:")} ${theme.fg("text", response.authMode === "oauth" ? "OAuth" : response.authMode === "api_key" ? "API key" : response.authMode)}`,
|