@sayknow-cli/coding-agent 0.3.9 → 0.3.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 +13 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/model-profile-activation.d.ts +7 -7
- package/dist/types/config/model-registry.d.ts +11 -3
- package/dist/types/config/model-resolver.d.ts +2 -2
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -2
- package/dist/types/modes/components/thinking-selector.d.ts +6 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +29 -0
- package/dist/types/notifications/index.d.ts +12 -1
- package/dist/types/notifications/threaded-render.d.ts +2 -0
- package/dist/types/notifications/topic-registry.d.ts +4 -5
- package/dist/types/runtime-credential-selector.d.ts +7 -0
- package/dist/types/sdk.d.ts +7 -1
- package/dist/types/session/agent-session.d.ts +6 -3
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/skill-discovery.d.ts +40 -0
- package/package.json +7 -7
- package/src/cli/args.ts +8 -0
- package/src/cli/notify-cli.ts +27 -1
- package/src/cli/update-cli.ts +24 -63
- package/src/cli.ts +5 -0
- package/src/commands/launch.ts +5 -0
- package/src/commands/notify.ts +2 -2
- package/src/config/model-profile-activation.ts +23 -7
- package/src/config/model-registry.ts +107 -11
- package/src/config/model-resolver.ts +5 -16
- package/src/config/settings-schema.ts +3 -2
- package/src/config/settings.ts +24 -1
- package/src/extensibility/runtime-skill-discovery.ts +229 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +26 -3
- package/src/modes/components/settings-selector.ts +3 -3
- package/src/modes/components/thinking-selector.ts +79 -18
- package/src/modes/controllers/input-controller.ts +50 -9
- package/src/modes/controllers/selector-controller.ts +7 -5
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +48 -15
- package/src/notifications/config-commands.ts +70 -0
- package/src/notifications/index.ts +156 -9
- package/src/notifications/rich-render.ts +1 -0
- package/src/notifications/telegram-daemon.ts +87 -4
- package/src/notifications/threaded-render.ts +14 -0
- package/src/notifications/topic-registry.ts +9 -8
- package/src/prompts/tools/skill-discovery.md +13 -0
- package/src/runtime-credential-selector.ts +35 -0
- package/src/sdk.ts +68 -5
- package/src/session/agent-session.ts +42 -18
- package/src/skc-runtime/psmux-detect.ts +17 -2
- package/src/system-prompt.ts +4 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/skill-discovery.ts +73 -0
- package/src/tools/skill.ts +15 -2
|
@@ -1,53 +1,114 @@
|
|
|
1
1
|
import { ThinkingLevel, type ThinkingLevel as ThinkingLevelValue } from "@sayknow-cli/agent-core";
|
|
2
|
-
import { Container, type SelectItem, SelectList } from "@sayknow-cli/tui";
|
|
3
|
-
import { getSelectListTheme } from "../../modes/theme/theme";
|
|
2
|
+
import { Container, type SelectItem, SelectList, Text } from "@sayknow-cli/tui";
|
|
3
|
+
import { getSelectListTheme, theme } from "../../modes/theme/theme";
|
|
4
4
|
import { getThinkingLevelMetadata, type ThinkingLevelValue as ThinkingMetadataValue } from "../../thinking-metadata";
|
|
5
5
|
import { DynamicBorder } from "./dynamic-border";
|
|
6
6
|
|
|
7
|
+
const SCOPE_ITEMS = [
|
|
8
|
+
{
|
|
9
|
+
value: "session",
|
|
10
|
+
label: "Apply for this session",
|
|
11
|
+
description: "Use this effort until the session changes it again",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
value: "default",
|
|
15
|
+
label: "Set as default",
|
|
16
|
+
description: "Save this effort for future sessions",
|
|
17
|
+
},
|
|
18
|
+
] as const satisfies readonly SelectItem[];
|
|
19
|
+
|
|
20
|
+
export interface ThinkingSelectorSelection {
|
|
21
|
+
level: ThinkingLevelValue;
|
|
22
|
+
persistDefault: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
/**
|
|
8
26
|
* Component that renders a thinking level selector with borders
|
|
9
27
|
*/
|
|
10
28
|
export class ThinkingSelectorComponent extends Container {
|
|
11
29
|
#selectList: SelectList;
|
|
30
|
+
#levelList: SelectList;
|
|
31
|
+
#selectedLevel: ThinkingLevelValue | undefined;
|
|
32
|
+
readonly #onSelect: (selection: ThinkingSelectorSelection) => void;
|
|
33
|
+
readonly #onCancel: () => void;
|
|
12
34
|
|
|
13
35
|
constructor(
|
|
14
36
|
currentLevel: ThinkingLevelValue | undefined,
|
|
15
37
|
availableLevels: ThinkingLevelValue[],
|
|
16
|
-
onSelect: (
|
|
38
|
+
onSelect: (selection: ThinkingSelectorSelection) => void,
|
|
17
39
|
onCancel: () => void,
|
|
18
40
|
) {
|
|
19
41
|
super();
|
|
20
42
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
43
|
+
this.#onSelect = onSelect;
|
|
44
|
+
this.#onCancel = onCancel;
|
|
24
45
|
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
const currentValue = currentLevel ?? ThinkingLevel.Off;
|
|
47
|
+
const thinkingLevels: SelectItem[] = availableLevels.map(level => {
|
|
48
|
+
const metadata = getThinkingLevelMetadata(level as ThinkingMetadataValue);
|
|
49
|
+
return level === currentValue
|
|
50
|
+
? { ...metadata, label: metadata.label + theme.fg("muted", " (current)") }
|
|
51
|
+
: metadata;
|
|
52
|
+
});
|
|
27
53
|
|
|
28
|
-
|
|
29
|
-
this.#selectList =
|
|
54
|
+
this.#levelList = new SelectList(thinkingLevels, thinkingLevels.length, getSelectListTheme());
|
|
55
|
+
this.#selectList = this.#levelList;
|
|
30
56
|
|
|
31
57
|
// Preselect current level
|
|
32
|
-
const currentIndex = thinkingLevels.findIndex(item => item.value ===
|
|
58
|
+
const currentIndex = thinkingLevels.findIndex(item => item.value === currentValue);
|
|
33
59
|
if (currentIndex !== -1) {
|
|
34
|
-
this.#
|
|
60
|
+
this.#levelList.setSelectedIndex(currentIndex);
|
|
35
61
|
}
|
|
36
62
|
|
|
37
|
-
this.#
|
|
38
|
-
|
|
63
|
+
this.#levelList.onSelect = item => {
|
|
64
|
+
this.#selectedLevel = item.value as ThinkingLevelValue;
|
|
65
|
+
this.#renderScopeList();
|
|
39
66
|
};
|
|
40
67
|
|
|
41
|
-
this.#
|
|
42
|
-
onCancel();
|
|
68
|
+
this.#levelList.onCancel = () => {
|
|
69
|
+
this.#onCancel();
|
|
43
70
|
};
|
|
44
71
|
|
|
45
|
-
this
|
|
72
|
+
this.#renderLevelList();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#renderLevelList(): void {
|
|
76
|
+
this.detachAll();
|
|
77
|
+
this.#selectedLevel = undefined;
|
|
78
|
+
this.#selectList = this.#levelList;
|
|
79
|
+
this.addChild(new DynamicBorder());
|
|
80
|
+
this.addChild(this.#levelList);
|
|
81
|
+
this.addChild(new DynamicBorder());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#renderScopeList(): void {
|
|
85
|
+
const level = this.#selectedLevel;
|
|
86
|
+
if (!level) return;
|
|
87
|
+
|
|
88
|
+
const scopeList = new SelectList(SCOPE_ITEMS, SCOPE_ITEMS.length, getSelectListTheme());
|
|
89
|
+
scopeList.onSelect = item => {
|
|
90
|
+
this.#onSelect({
|
|
91
|
+
level,
|
|
92
|
+
persistDefault: item.value === "default",
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
scopeList.onCancel = () => {
|
|
96
|
+
this.#renderLevelList();
|
|
97
|
+
};
|
|
46
98
|
|
|
47
|
-
|
|
99
|
+
const metadata = getThinkingLevelMetadata(level as ThinkingMetadataValue);
|
|
100
|
+
this.detachAll();
|
|
101
|
+
this.#selectList = scopeList;
|
|
102
|
+
this.addChild(new DynamicBorder());
|
|
103
|
+
this.addChild(new Text(theme.fg("muted", ` Reasoning effort: ${metadata.label}`), 0, 0));
|
|
104
|
+
this.addChild(scopeList);
|
|
48
105
|
this.addChild(new DynamicBorder());
|
|
49
106
|
}
|
|
50
107
|
|
|
108
|
+
handleInput(data: string): void {
|
|
109
|
+
this.#selectList.handleInput(data);
|
|
110
|
+
}
|
|
111
|
+
|
|
51
112
|
getSelectList(): SelectList {
|
|
52
113
|
return this.#selectList;
|
|
53
114
|
}
|
|
@@ -27,6 +27,8 @@ interface Expandable {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
|
|
30
|
+
const IMAGE_PLACEHOLDER_PATTERN = /\[image ([1-9]\d*)\]/g;
|
|
31
|
+
const IMAGE_PLACEHOLDER_PRESENT_PATTERN = /\[image [1-9]\d*\]/;
|
|
30
32
|
|
|
31
33
|
function isExpandable(obj: unknown): obj is Expandable {
|
|
32
34
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
@@ -383,6 +385,7 @@ export class InputController {
|
|
|
383
385
|
this.ctx.isBashMode = trimmed.startsWith("!");
|
|
384
386
|
this.ctx.isBashNoContext = trimmed.startsWith("!!");
|
|
385
387
|
this.ctx.isPythonMode = trimmed.startsWith("$") && !trimmed.startsWith("${");
|
|
388
|
+
this.#clearPendingImagesIfPlaceholdersRemoved(text);
|
|
386
389
|
if (
|
|
387
390
|
wasBashMode !== this.ctx.isBashMode ||
|
|
388
391
|
wasBashNoContext !== this.ctx.isBashNoContext ||
|
|
@@ -418,7 +421,7 @@ export class InputController {
|
|
|
418
421
|
}
|
|
419
422
|
|
|
420
423
|
const runner = this.ctx.session.extensionRunner;
|
|
421
|
-
let inputImages = this
|
|
424
|
+
let inputImages = this.#visiblePendingImagesForText(text);
|
|
422
425
|
|
|
423
426
|
if (runner?.hasHandlers("input")) {
|
|
424
427
|
const result = await runner.emitInput(text, inputImages, "interactive");
|
|
@@ -498,7 +501,7 @@ export class InputController {
|
|
|
498
501
|
|
|
499
502
|
// Queue input during compaction
|
|
500
503
|
if (this.ctx.session.isCompacting) {
|
|
501
|
-
if (
|
|
504
|
+
if ((inputImages?.length ?? 0) > 0) {
|
|
502
505
|
this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
|
|
503
506
|
return;
|
|
504
507
|
}
|
|
@@ -520,11 +523,13 @@ export class InputController {
|
|
|
520
523
|
// typed since queuing intact. Same protection as #783, applied to
|
|
521
524
|
// the streaming/queue path.
|
|
522
525
|
const streamingBehavior = this.#busyStreamingBehavior();
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
)
|
|
526
|
+
const promptOptions =
|
|
527
|
+
streamingBehavior === "followUp"
|
|
528
|
+
? { streamingBehavior, images, followUpQueuePolicy: "sequential" as const }
|
|
529
|
+
: { streamingBehavior, images };
|
|
530
|
+
await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, promptOptions), {
|
|
531
|
+
imageCount: images?.length ?? 0,
|
|
532
|
+
});
|
|
528
533
|
this.ctx.updatePendingMessagesDisplay();
|
|
529
534
|
this.ctx.ui.requestRender();
|
|
530
535
|
return;
|
|
@@ -869,7 +874,9 @@ export class InputController {
|
|
|
869
874
|
details,
|
|
870
875
|
attribution: "user",
|
|
871
876
|
},
|
|
872
|
-
|
|
877
|
+
streamingBehavior === "followUp"
|
|
878
|
+
? { streamingBehavior, followUpQueuePolicy: "sequential" }
|
|
879
|
+
: { streamingBehavior },
|
|
873
880
|
);
|
|
874
881
|
}
|
|
875
882
|
if (this.ctx.session.isStreaming) {
|
|
@@ -911,7 +918,10 @@ export class InputController {
|
|
|
911
918
|
this.ctx.editor.addToHistory(text);
|
|
912
919
|
this.ctx.editor.setText("");
|
|
913
920
|
await this.ctx.withLocalSubmission(text, () =>
|
|
914
|
-
this.ctx.session.prompt(text, {
|
|
921
|
+
this.ctx.session.prompt(text, {
|
|
922
|
+
streamingBehavior: "followUp",
|
|
923
|
+
followUpQueuePolicy: "sequential",
|
|
924
|
+
}),
|
|
915
925
|
);
|
|
916
926
|
this.ctx.updatePendingMessagesDisplay();
|
|
917
927
|
this.ctx.ui.requestRender();
|
|
@@ -1104,6 +1114,37 @@ export class InputController {
|
|
|
1104
1114
|
return `[image ${this.ctx.pendingImages.length}]`;
|
|
1105
1115
|
}
|
|
1106
1116
|
|
|
1117
|
+
#visiblePendingImagesForText(text: string): InteractiveModeContext["pendingImages"] | undefined {
|
|
1118
|
+
if (this.ctx.pendingImages.length === 0) {
|
|
1119
|
+
return undefined;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const images: InteractiveModeContext["pendingImages"] = [];
|
|
1123
|
+
const seenImageIndexes = new Set<number>();
|
|
1124
|
+
for (const match of text.matchAll(IMAGE_PLACEHOLDER_PATTERN)) {
|
|
1125
|
+
const placeholderNumberText = match[1];
|
|
1126
|
+
if (!placeholderNumberText) continue;
|
|
1127
|
+
const placeholderNumber = Number.parseInt(placeholderNumberText, 10);
|
|
1128
|
+
const imageIndex = placeholderNumber - 1;
|
|
1129
|
+
if (imageIndex < 0 || imageIndex >= this.ctx.pendingImages.length || seenImageIndexes.has(imageIndex)) {
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
const image = this.ctx.pendingImages[imageIndex];
|
|
1133
|
+
if (!image) continue;
|
|
1134
|
+
images.push(image);
|
|
1135
|
+
seenImageIndexes.add(imageIndex);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return images.length > 0 ? images : undefined;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
#clearPendingImagesIfPlaceholdersRemoved(text: string): void {
|
|
1142
|
+
if (this.ctx.pendingImages.length === 0 || IMAGE_PLACEHOLDER_PRESENT_PATTERN.test(text)) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
this.ctx.pendingImages = [];
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1107
1148
|
async handleImagePaste(): Promise<boolean> {
|
|
1108
1149
|
try {
|
|
1109
1150
|
const image = await readImageFromClipboard();
|
|
@@ -340,12 +340,13 @@ export class SelectorController {
|
|
|
340
340
|
const selector = new ThinkingSelectorComponent(
|
|
341
341
|
this.ctx.session.thinkingLevel,
|
|
342
342
|
availableLevels,
|
|
343
|
-
|
|
343
|
+
selection => {
|
|
344
344
|
done();
|
|
345
345
|
|
|
346
|
+
const { level, persistDefault } = selection;
|
|
346
347
|
const configuredDefault = this.ctx.settings.get("defaultThinkingLevel");
|
|
347
348
|
const levelToApply = level === ThinkingLevel.Inherit ? configuredDefault : level;
|
|
348
|
-
this.ctx.session.setThinkingLevel(levelToApply,
|
|
349
|
+
this.ctx.session.setThinkingLevel(levelToApply, persistDefault);
|
|
349
350
|
const effectiveLevel = this.ctx.session.thinkingLevel ?? ThinkingLevel.Off;
|
|
350
351
|
const requestedLabel =
|
|
351
352
|
level === ThinkingLevel.Inherit ? `${level} (configured default: ${configuredDefault})` : level;
|
|
@@ -355,9 +356,11 @@ export class SelectorController {
|
|
|
355
356
|
this.ctx.statusLine.invalidate();
|
|
356
357
|
this.ctx.updateEditorBorderColor();
|
|
357
358
|
this.ctx.updateEditorTopBorder();
|
|
359
|
+
if (persistDefault) void this.ctx.notifyConfigChanged?.();
|
|
358
360
|
this.ctx.ui.requestRender();
|
|
361
|
+
const scopeLabel = persistDefault ? "Default reasoning effort" : "Reasoning effort";
|
|
359
362
|
this.ctx.showStatus(
|
|
360
|
-
|
|
363
|
+
`${scopeLabel} set to ${requestedLabel}. Effective effort: ${effectiveLevel}.${clampedSuffix}`,
|
|
361
364
|
);
|
|
362
365
|
},
|
|
363
366
|
() => {
|
|
@@ -365,10 +368,9 @@ export class SelectorController {
|
|
|
365
368
|
this.ctx.ui.requestRender();
|
|
366
369
|
},
|
|
367
370
|
);
|
|
368
|
-
return { component: selector, focus: selector
|
|
371
|
+
return { component: selector, focus: selector };
|
|
369
372
|
});
|
|
370
373
|
}
|
|
371
|
-
|
|
372
374
|
showSettingsSelector(): void {
|
|
373
375
|
getAvailableThemes().then(availableThemes => {
|
|
374
376
|
this.showSelector(done => {
|
package/src/modes/types.ts
CHANGED
|
@@ -37,17 +37,27 @@ interface RenderInitialMessagesOptions {
|
|
|
37
37
|
preserveExistingChat?: boolean;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function cloneRenderArgs(args: Record<string, unknown>): Record<string, unknown> {
|
|
41
|
+
try {
|
|
42
|
+
return structuredClone(args);
|
|
43
|
+
} catch {
|
|
44
|
+
return { ...args };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
export function argsWithPartialJson(args: unknown, partialJson: unknown): unknown {
|
|
41
49
|
if (typeof partialJson !== "string" || !args || typeof args !== "object" || Array.isArray(args)) return args;
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
50
|
+
// Keep the transient streaming buffer on a renderer-only snapshot. The live
|
|
51
|
+
// assistant message args are later validated/executed, so UI-only metadata or
|
|
52
|
+
// renderer mutations must never share that object reference.
|
|
53
|
+
const renderArgs = cloneRenderArgs(args as Record<string, unknown>);
|
|
54
|
+
Object.defineProperty(renderArgs, "__partialJson", {
|
|
45
55
|
value: partialJson,
|
|
46
56
|
enumerable: false,
|
|
47
57
|
configurable: true,
|
|
48
58
|
writable: true,
|
|
49
59
|
});
|
|
50
|
-
return
|
|
60
|
+
return renderArgs;
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
type QueuedMessages = {
|
|
@@ -625,7 +635,11 @@ export class UiHelpers {
|
|
|
625
635
|
}
|
|
626
636
|
|
|
627
637
|
queueCompactionMessage(text: string, mode: "steer" | "followUp"): void {
|
|
628
|
-
|
|
638
|
+
const entry: CompactionQueuedMessage = { text, mode };
|
|
639
|
+
if (mode === "followUp") {
|
|
640
|
+
entry.followUpQueuePolicy = "sequential";
|
|
641
|
+
}
|
|
642
|
+
this.ctx.compactionQueuedMessages.push(entry);
|
|
629
643
|
this.ctx.editor.addToHistory(text);
|
|
630
644
|
this.ctx.editor.setText("");
|
|
631
645
|
this.ctx.updatePendingMessagesDisplay();
|
|
@@ -639,6 +653,11 @@ export class UiHelpers {
|
|
|
639
653
|
return this.#hasSkillInvocations(text) || this.isKnownSlashCommand(text);
|
|
640
654
|
}
|
|
641
655
|
|
|
656
|
+
#compactionFollowUpQueuePolicy(message: CompactionQueuedMessage): "sequential" | undefined {
|
|
657
|
+
if (message.mode !== "followUp") return undefined;
|
|
658
|
+
return message.followUpQueuePolicy ?? "sequential";
|
|
659
|
+
}
|
|
660
|
+
|
|
642
661
|
async #deliverQueuedSkillMessage(message: CompactionQueuedMessage): Promise<boolean> {
|
|
643
662
|
const invocations = parseSkillInvocations(message.text, this.ctx.skillCommands ?? new Map());
|
|
644
663
|
if (invocations.length === 0) {
|
|
@@ -681,6 +700,13 @@ export class UiHelpers {
|
|
|
681
700
|
continue;
|
|
682
701
|
}
|
|
683
702
|
|
|
703
|
+
const promptOptions =
|
|
704
|
+
message.mode === "followUp"
|
|
705
|
+
? {
|
|
706
|
+
streamingBehavior: message.mode,
|
|
707
|
+
followUpQueuePolicy: this.#compactionFollowUpQueuePolicy(message),
|
|
708
|
+
}
|
|
709
|
+
: { streamingBehavior: message.mode };
|
|
684
710
|
await this.ctx.session.promptCustomMessage(
|
|
685
711
|
{
|
|
686
712
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
@@ -689,7 +715,7 @@ export class UiHelpers {
|
|
|
689
715
|
details,
|
|
690
716
|
attribution: "user",
|
|
691
717
|
},
|
|
692
|
-
|
|
718
|
+
promptOptions,
|
|
693
719
|
);
|
|
694
720
|
}
|
|
695
721
|
|
|
@@ -709,7 +735,11 @@ export class UiHelpers {
|
|
|
709
735
|
return;
|
|
710
736
|
}
|
|
711
737
|
await this.ctx.withLocalSubmission(message.text, () =>
|
|
712
|
-
message.mode === "followUp"
|
|
738
|
+
message.mode === "followUp"
|
|
739
|
+
? this.ctx.session.followUp(message.text, undefined, {
|
|
740
|
+
followUpQueuePolicy: this.#compactionFollowUpQueuePolicy(message),
|
|
741
|
+
})
|
|
742
|
+
: this.ctx.session.steer(message.text),
|
|
713
743
|
);
|
|
714
744
|
}
|
|
715
745
|
|
|
@@ -800,14 +830,17 @@ export class UiHelpers {
|
|
|
800
830
|
// `restoreQueue` rather than rethrown, so we use the primitive
|
|
801
831
|
// recordLocalSubmission and dispose manually in the catch.
|
|
802
832
|
const disposeFirstPrompt = this.ctx.recordLocalSubmission(firstPrompt.text);
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
833
|
+
const firstPromptOptions =
|
|
834
|
+
firstPrompt.mode === "followUp"
|
|
835
|
+
? {
|
|
836
|
+
streamingBehavior: "followUp" as const,
|
|
837
|
+
followUpQueuePolicy: this.#compactionFollowUpQueuePolicy(firstPrompt),
|
|
838
|
+
}
|
|
839
|
+
: { streamingBehavior: "steer" as const };
|
|
840
|
+
const promptPromise = this.ctx.session.prompt(firstPrompt.text, firstPromptOptions).catch((error: unknown) => {
|
|
841
|
+
disposeFirstPrompt();
|
|
842
|
+
restoreQueue(error);
|
|
843
|
+
});
|
|
811
844
|
|
|
812
845
|
for (const message of rest) {
|
|
813
846
|
await this.#deliverQueuedMessage(message);
|
|
@@ -20,6 +20,76 @@ export interface ConfigCommandChange {
|
|
|
20
20
|
redact?: boolean;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export type TelegramControlCommandName = "reasoning" | "usage" | "context" | "compact";
|
|
24
|
+
|
|
25
|
+
export type TelegramControlCommand =
|
|
26
|
+
| { name: "reasoning"; action: "cycle" | "status" | "set"; level?: string }
|
|
27
|
+
| { name: "usage" }
|
|
28
|
+
| { name: "context" }
|
|
29
|
+
| { name: "compact"; instructions?: string };
|
|
30
|
+
|
|
31
|
+
export type TelegramControlCommandParseResult =
|
|
32
|
+
| { kind: "none" }
|
|
33
|
+
| { kind: "ignored"; commandName: TelegramControlCommandName }
|
|
34
|
+
| { kind: "command"; command: TelegramControlCommand }
|
|
35
|
+
| { kind: "invalid"; commandName: TelegramControlCommandName; usage: string };
|
|
36
|
+
|
|
37
|
+
const TELEGRAM_CONTROL_COMMANDS = new Set<TelegramControlCommandName>(["reasoning", "usage", "context", "compact"]);
|
|
38
|
+
const TELEGRAM_REASONING_LEVELS = new Set(["inherit", "off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
39
|
+
|
|
40
|
+
function splitTelegramBotSuffix(rawCommand: string): { name: string; suffix?: string } {
|
|
41
|
+
const [name, suffix] = rawCommand.toLowerCase().split("@", 2);
|
|
42
|
+
return suffix ? { name, suffix } : { name };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function telegramControlCommandUsage(commandName: TelegramControlCommandName): string {
|
|
46
|
+
switch (commandName) {
|
|
47
|
+
case "reasoning":
|
|
48
|
+
return "Usage: /reasoning [cycle|inherit|off|minimal|low|medium|high|xhigh|max]";
|
|
49
|
+
case "usage":
|
|
50
|
+
return "Usage: /usage";
|
|
51
|
+
case "context":
|
|
52
|
+
return "Usage: /context";
|
|
53
|
+
case "compact":
|
|
54
|
+
return "Usage: /compact [instructions]";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Parse deterministic Telegram session-control commands. Recognised roots fail closed. */
|
|
59
|
+
export function parseTelegramControlCommand(text: string, botUsername?: string): TelegramControlCommandParseResult {
|
|
60
|
+
const trimmed = text.trim();
|
|
61
|
+
if (!trimmed.startsWith("/")) return { kind: "none" };
|
|
62
|
+
const [rawRoot, ...rest] = trimmed.slice(1).split(/\s+/);
|
|
63
|
+
if (!rawRoot) return { kind: "none" };
|
|
64
|
+
const { name: root, suffix } = splitTelegramBotSuffix(rawRoot);
|
|
65
|
+
if (!TELEGRAM_CONTROL_COMMANDS.has(root as TelegramControlCommandName)) return { kind: "none" };
|
|
66
|
+
const commandName = root as TelegramControlCommandName;
|
|
67
|
+
if (suffix && (!botUsername || suffix !== botUsername.toLowerCase())) return { kind: "ignored", commandName };
|
|
68
|
+
const usage = telegramControlCommandUsage(commandName);
|
|
69
|
+
|
|
70
|
+
switch (commandName) {
|
|
71
|
+
case "usage":
|
|
72
|
+
case "context":
|
|
73
|
+
return rest.length === 0
|
|
74
|
+
? { kind: "command", command: { name: commandName } }
|
|
75
|
+
: { kind: "invalid", commandName, usage };
|
|
76
|
+
case "compact": {
|
|
77
|
+
const instructions = trimmed.slice(rawRoot.length + 1).trim();
|
|
78
|
+
return { kind: "command", command: instructions ? { name: "compact", instructions } : { name: "compact" } };
|
|
79
|
+
}
|
|
80
|
+
case "reasoning": {
|
|
81
|
+
if (rest.length === 0) return { kind: "command", command: { name: "reasoning", action: "status" } };
|
|
82
|
+
if (rest.length !== 1) return { kind: "invalid", commandName, usage };
|
|
83
|
+
const levelOrAction = rest[0]!.toLowerCase();
|
|
84
|
+
if (levelOrAction === "cycle") return { kind: "command", command: { name: "reasoning", action: "cycle" } };
|
|
85
|
+
if (TELEGRAM_REASONING_LEVELS.has(levelOrAction)) {
|
|
86
|
+
return { kind: "command", command: { name: "reasoning", action: "set", level: levelOrAction } };
|
|
87
|
+
}
|
|
88
|
+
return { kind: "invalid", commandName, usage };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
23
93
|
/**
|
|
24
94
|
* Parse an in-thread config command. Returns the requested change, or
|
|
25
95
|
* `undefined` when the text is not a recognised config command (so the daemon
|