@selesai/code 0.13.8 → 0.13.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/core/agent-session.d.ts +6 -0
  3. package/dist/core/agent-session.js +5 -3
  4. package/dist/core/settings-manager.d.ts +11 -0
  5. package/dist/core/settings-manager.js +48 -0
  6. package/dist/extensions/auto-session-name.test.ts +13 -1
  7. package/dist/extensions/auto-session-name.ts +8 -4
  8. package/dist/extensions/handoff-new.test.ts +26 -1
  9. package/dist/extensions/handoff-new.ts +5 -0
  10. package/dist/extensions/pi-subagents/README.md +0 -6
  11. package/dist/extensions/pi-subagents/docs/agents.md +1 -114
  12. package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +1 -12
  13. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
  14. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +3 -19
  15. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-contract.ts +8 -37
  16. package/dist/extensions/pi-subagents/src/shared/types.ts +2 -2
  17. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +2 -38
  18. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +0 -49
  19. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +0 -18
  20. package/dist/modes/rpc/rpc-client.d.ts +180 -10
  21. package/dist/modes/rpc/rpc-client.js +211 -13
  22. package/dist/modes/rpc/rpc-mode.d.ts +1 -1
  23. package/dist/modes/rpc/rpc-mode.js +682 -3
  24. package/dist/modes/rpc/rpc-types.d.ts +340 -2
  25. package/dist/skills/improve-codebase/SKILL.md +1 -1
  26. package/dist/skills/planger/SKILL.md +1 -1
  27. package/dist/skills/workflow/SKILL.md +9 -15
  28. package/docs/rpc.md +171 -1
  29. package/package.json +1 -1
  30. package/dist/extensions/pi-subagents/agents/architect.md +0 -170
  31. package/dist/extensions/pi-subagents/agents/builder.md +0 -37
  32. package/dist/extensions/pi-subagents/agents/claude-code-writer.md +0 -15
  33. package/dist/extensions/pi-subagents/agents/claude-code.md +0 -15
  34. package/dist/extensions/pi-subagents/agents/codex-exec-writer.md +0 -15
  35. package/dist/extensions/pi-subagents/agents/codex-exec.md +0 -15
  36. package/dist/extensions/pi-subagents/agents/commentator.md +0 -37
  37. package/dist/extensions/pi-subagents/agents/cursor-agent-writer.md +0 -14
  38. package/dist/extensions/pi-subagents/agents/cursor-agent.md +0 -14
  39. package/dist/extensions/pi-subagents/agents/explorer.md +0 -32
  40. package/dist/extensions/pi-subagents/agents/recapper.md +0 -31
  41. package/dist/extensions/pi-subagents/src/runs/shared/claude-code-adapter.ts +0 -129
  42. package/dist/extensions/pi-subagents/src/runs/shared/codex-exec-adapter.ts +0 -129
  43. package/dist/extensions/pi-subagents/src/runs/shared/cursor-agent-adapter.ts +0 -114
  44. package/dist/extensions/pi-subagents/test/integration/claude-code-smoke.test.ts +0 -52
  45. package/dist/extensions/pi-subagents/test/integration/claude-code-writer-smoke.test.ts +0 -55
  46. package/dist/extensions/pi-subagents/test/integration/codex-exec-smoke.test.ts +0 -53
  47. package/dist/extensions/pi-subagents/test/integration/codex-exec-writer-smoke.test.ts +0 -57
  48. package/dist/extensions/pi-subagents/test/integration/cursor-agent-smoke.test.ts +0 -59
  49. package/dist/extensions/pi-subagents/test/integration/cursor-agent-writer-smoke.test.ts +0 -62
  50. package/dist/extensions/pi-subagents/test/unit/claude-code-adapter.test.ts +0 -245
  51. package/dist/extensions/pi-subagents/test/unit/codex-exec-adapter.test.ts +0 -192
  52. package/dist/extensions/pi-subagents/test/unit/cursor-agent-adapter.test.ts +0 -259
@@ -11,10 +11,26 @@
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
13
  import * as crypto from "node:crypto";
14
+ import { spawnSync } from "node:child_process";
15
+ import { chmodSync, copyFileSync, existsSync, unlinkSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
14
18
  import { generateHandoff } from "../../core/handoff.js";
19
+ import { buildGitCommandPrompt } from "../../core/git-command.js";
20
+ import { KeybindingsManager } from "../../core/keybindings.js";
21
+ import { SessionManager } from "../../core/session-manager.js";
22
+ import { resolveModelScope } from "../../core/model-resolver.js";
23
+ import { refreshModelCatalogs } from "../interactive/model-catalog-refresh.js";
15
24
  import { flushRawStdout, takeOverStdout, waitForRawStdoutBackpressure, writeRawStdout, } from "../../core/output-guard.js";
25
+ import { ProjectTrustStore, hasTrustRequiringProjectResources } from "../../core/trust-manager.js";
26
+ import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
27
+ import { createSyntheticSourceInfo } from "../../core/source-info.js";
16
28
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
29
+ import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
30
+ import { VERSION, getAgentDir, getBundledDefaultsDir, getShareViewerUrl } from "../../config.js";
31
+ import { openBrowser } from "../../utils/open-browser.js";
17
32
  import { theme } from "../interactive/theme/theme.js";
33
+ import { getAvailableThemesWithPaths, setRegisteredThemes } from "../interactive/theme/theme.js";
18
34
  import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
19
35
  /**
20
36
  * Run in RPC mode.
@@ -28,6 +44,43 @@ export async function runRpcMode(runtimeHost) {
28
44
  const output = (obj) => {
29
45
  writeRawStdout(serializeJsonLine(obj));
30
46
  };
47
+ /**
48
+ * Filter a session tree with the same semantics as the TUI tree selector
49
+ * (tree-selector.ts FilterMode). Hidden leaf pruning is preserved.
50
+ */
51
+ const filterTree = (nodes, mode) => {
52
+ const passes = (node) => {
53
+ const entry = node.entry;
54
+ const isSettingsEntry = entry.type === "label" ||
55
+ entry.type === "custom" ||
56
+ entry.type === "model_change" ||
57
+ entry.type === "thinking_level_change" ||
58
+ entry.type === "session_info";
59
+ switch (mode) {
60
+ case "user-only":
61
+ return entry.type === "message" && entry.message.role === "user";
62
+ case "no-tools":
63
+ return !isSettingsEntry && !(entry.type === "message" && entry.message.role === "toolResult");
64
+ case "labeled-only":
65
+ return node.label !== undefined;
66
+ case "all":
67
+ return true;
68
+ default:
69
+ return !isSettingsEntry;
70
+ }
71
+ };
72
+ const visit = (nodes) => {
73
+ const result = [];
74
+ for (const node of nodes) {
75
+ if (!passes(node))
76
+ continue;
77
+ const children = visit(node.children);
78
+ result.push({ ...node, children });
79
+ }
80
+ return result;
81
+ };
82
+ return visit(nodes);
83
+ };
31
84
  const success = (id, command, data) => {
32
85
  if (data === undefined) {
33
86
  return { id, type: "response", command, success: true };
@@ -39,6 +92,8 @@ export async function runRpcMode(runtimeHost) {
39
92
  };
40
93
  // Pending extension UI requests waiting for response
41
94
  const pendingExtensionRequests = new Map();
95
+ // Prompts buffered while compacting (queueWhileCompacting) — flushed after compaction_end.
96
+ const compactionPromptQueue = [];
42
97
  // Shutdown request flag
43
98
  let shutdownRequested = false;
44
99
  let shuttingDown = false;
@@ -268,6 +323,30 @@ export async function runRpcMode(runtimeHost) {
268
323
  if (event.type === "agent_settled") {
269
324
  void checkShutdownRequested();
270
325
  }
326
+ if (event.type === "compaction_end" && compactionPromptQueue.length > 0) {
327
+ // TUI flushCompactionQueue parity: replay buffered prompts after compaction finishes.
328
+ // Completed compaction replays in-order (extension commands via prompt, rest by
329
+ // their original streaming behavior); aborted compaction discards the buffer.
330
+ if (event.aborted || event.errorMessage) {
331
+ compactionPromptQueue.length = 0;
332
+ return;
333
+ }
334
+ const queued = compactionPromptQueue.splice(0);
335
+ void (async () => {
336
+ for (const item of queued) {
337
+ try {
338
+ await session.prompt(item.message, {
339
+ images: item.images,
340
+ streamingBehavior: item.streamingBehavior,
341
+ source: "rpc",
342
+ });
343
+ }
344
+ catch {
345
+ // Replay failures surface through agent error handling; do not kill the stream.
346
+ }
347
+ }
348
+ })();
349
+ }
271
350
  });
272
351
  unsubscribeBackpressure = session.agent.subscribe(async () => {
273
352
  await waitForRawStdoutBackpressure();
@@ -300,6 +379,21 @@ export async function runRpcMode(runtimeHost) {
300
379
  // Start prompt handling immediately, but emit the authoritative response only after
301
380
  // prompt preflight succeeds. Queued and immediately handled prompts also count as success.
302
381
  let preflightSucceeded = false;
382
+ // During compaction: either buffer and auto-send after compaction_end (TUI parity),
383
+ // or fail fast with a clear error when the caller did not opt in.
384
+ if (session.isCompacting) {
385
+ if (!command.queueWhileCompacting) {
386
+ return error(id, "prompt", "Agent is compacting; set queueWhileCompacting:true to buffer this prompt and send it after compaction, or wait for compaction_end.");
387
+ }
388
+ const queued = {
389
+ message: command.message,
390
+ images: command.images,
391
+ streamingBehavior: command.streamingBehavior,
392
+ };
393
+ output(success(id, "prompt")); // accepted into the buffer
394
+ compactionPromptQueue.push(queued);
395
+ return undefined;
396
+ }
303
397
  void session
304
398
  .prompt(command.message, {
305
399
  images: command.images,
@@ -328,6 +422,10 @@ export async function runRpcMode(runtimeHost) {
328
422
  return success(id, "follow_up");
329
423
  }
330
424
  case "abort": {
425
+ // TUI Esc parity: cancel in-flight branch summarization and compaction as well,
426
+ // not just the active agent turn (session.abort() alone covers retry + agent).
427
+ session.abortBranchSummary();
428
+ session.abortCompaction();
331
429
  await session.abort();
332
430
  return success(id, "abort");
333
431
  }
@@ -418,13 +516,22 @@ export async function runRpcMode(runtimeHost) {
418
516
  return success(id, "set_model", model);
419
517
  }
420
518
  case "cycle_model": {
421
- const result = await session.cycleModel();
519
+ const result = await session.cycleModel(command.direction ?? "forward");
422
520
  if (!result) {
423
521
  return success(id, "cycle_model", null);
424
522
  }
425
523
  return success(id, "cycle_model", result);
426
524
  }
427
525
  case "get_available_models": {
526
+ if (command.refresh) {
527
+ // TUI parity: refresh model catalogs before listing (shared coordinator).
528
+ try {
529
+ await refreshModelCatalogs(session.modelRuntime, AbortSignal.timeout(15_000));
530
+ }
531
+ catch {
532
+ // Fall back to the current snapshot if the network refresh fails.
533
+ }
534
+ }
428
535
  const models = await session.modelRuntime.getAvailable();
429
536
  return success(id, "get_available_models", { models });
430
537
  }
@@ -530,7 +637,7 @@ export async function runRpcMode(runtimeHost) {
530
637
  return success(id, "get_session_stats", stats);
531
638
  }
532
639
  case "export_html": {
533
- const path = await session.exportToHtml(command.outputPath);
640
+ const path = await session.exportToHtml(command.outputPath, { themeName: command.themeName });
534
641
  return success(id, "export_html", { path });
535
642
  }
536
643
  case "switch_session": {
@@ -576,7 +683,10 @@ export async function runRpcMode(runtimeHost) {
576
683
  }
577
684
  case "get_tree": {
578
685
  const sessionManager = session.sessionManager;
579
- return success(id, "get_tree", { tree: sessionManager.getTree(), leafId: sessionManager.getLeafId() });
686
+ const tree = sessionManager.getTree();
687
+ const filter = command.filter ?? "default";
688
+ const filtered = filter === "all" ? tree : filterTree(tree, filter);
689
+ return success(id, "get_tree", { tree: filtered, leafId: sessionManager.getLeafId() });
580
690
  }
581
691
  case "get_last_assistant_text": {
582
692
  const text = session.getLastAssistantText();
@@ -591,6 +701,145 @@ export async function runRpcMode(runtimeHost) {
591
701
  return success(id, "set_session_name");
592
702
  }
593
703
  // =================================================================
704
+ // Tree navigation
705
+ // =================================================================
706
+ case "navigate_tree": {
707
+ if (session.isStreaming) {
708
+ return error(id, "navigate_tree", "Wait for the current response to finish before navigating the session tree.");
709
+ }
710
+ try {
711
+ const result = await session.navigateTree(command.targetId, {
712
+ summarize: command.summarize,
713
+ customInstructions: command.customInstructions,
714
+ replaceInstructions: command.replaceInstructions,
715
+ label: command.label,
716
+ });
717
+ return success(id, "navigate_tree", {
718
+ editorText: result.editorText,
719
+ cancelled: result.cancelled,
720
+ aborted: result.aborted,
721
+ summaryEntry: result.summaryEntry ?? null,
722
+ });
723
+ }
724
+ catch (navigationError) {
725
+ return error(id, "navigate_tree", navigationError instanceof Error ? navigationError.message : String(navigationError));
726
+ }
727
+ }
728
+ // =================================================================
729
+ // Session discovery / management
730
+ // =================================================================
731
+ case "list_sessions": {
732
+ try {
733
+ const sessionManager = session.sessionManager;
734
+ const isAll = command.scope === "all";
735
+ const source = isAll
736
+ ? sessionManager.usesDefaultSessionDir()
737
+ ? await SessionManager.listAll()
738
+ : await SessionManager.listAll(sessionManager.getSessionDir())
739
+ : await SessionManager.list(sessionManager.getCwd(), sessionManager.getSessionDir());
740
+ const sessions = source.map((info) => ({
741
+ path: info.path,
742
+ id: info.id,
743
+ cwd: info.cwd,
744
+ name: info.name,
745
+ parentSessionPath: info.parentSessionPath,
746
+ created: Number.isNaN(info.created.getTime()) ? new Date(0).toISOString() : info.created.toISOString(),
747
+ modified: Number.isNaN(info.modified.getTime()) ? new Date(0).toISOString() : info.modified.toISOString(),
748
+ messageCount: info.messageCount,
749
+ firstMessage: info.firstMessage,
750
+ }));
751
+ return success(id, "list_sessions", { sessions });
752
+ }
753
+ catch (listError) {
754
+ return error(id, "list_sessions", listError instanceof Error ? listError.message : String(listError));
755
+ }
756
+ }
757
+ case "rename_session": {
758
+ const name = command.name.trim();
759
+ if (!name) {
760
+ return error(id, "rename_session", "Session name cannot be empty");
761
+ }
762
+ if (!existsSync(command.path)) {
763
+ return error(id, "rename_session", `Session file not found: ${command.path}`);
764
+ }
765
+ try {
766
+ const target = SessionManager.open(command.path);
767
+ target.appendSessionInfo(name);
768
+ return success(id, "rename_session", { path: command.path, name });
769
+ }
770
+ catch (renameError) {
771
+ return error(id, "rename_session", renameError instanceof Error ? renameError.message : String(renameError));
772
+ }
773
+ }
774
+ case "delete_session": {
775
+ if (command.path === session.sessionFile) {
776
+ return error(id, "delete_session", "Cannot delete the currently active session.");
777
+ }
778
+ if (!existsSync(command.path)) {
779
+ return error(id, "delete_session", `Session file not found: ${command.path}`);
780
+ }
781
+ try {
782
+ // trash-CLI-first with unlink fallback (identical to the TUI session selector).
783
+ const trashArgs = command.path.startsWith("-") ? ["--", command.path] : [command.path];
784
+ const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" });
785
+ if (trashResult.status === 0 || !existsSync(command.path)) {
786
+ return success(id, "delete_session", { path: command.path, method: "trash" });
787
+ }
788
+ unlinkSync(command.path);
789
+ return success(id, "delete_session", { path: command.path, method: "unlink" });
790
+ }
791
+ catch (deleteError) {
792
+ return error(id, "delete_session", deleteError instanceof Error ? deleteError.message : String(deleteError));
793
+ }
794
+ }
795
+ // =================================================================
796
+ // Session-only scoped models (ephemeral, not persisted)
797
+ // =================================================================
798
+ case "set_session_models": {
799
+ try {
800
+ if (command.reorder !== undefined && command.reorder.length > 0) {
801
+ // Reorder-only path: rebuild scope from the reordered pattern list.
802
+ const scoped = await resolveModelScope(command.reorder, session.modelRuntime);
803
+ session.setScopedModels(scoped.map((sm) => ({ model: sm.model, thinkingLevel: sm.thinkingLevel })));
804
+ return success(id, "set_session_models", {
805
+ enabled: scoped.map((sm) => `${sm.model.provider}/${sm.model.id}`),
806
+ models: scoped.map((sm) => sm.model),
807
+ });
808
+ }
809
+ if (command.enabled === undefined) {
810
+ return error(id, "set_session_models", "Provide either enabled or reorder.");
811
+ }
812
+ if (command.enabled.length === 0) {
813
+ session.setScopedModels([]);
814
+ return success(id, "set_session_models", { enabled: [], models: [] });
815
+ }
816
+ const patterns = command.enabled;
817
+ const allModels = await session.modelRuntime.getAvailable();
818
+ const allModelIds = new Set(allModels.map((model) => `${model.provider}/${model.id}`));
819
+ const allEnabled = patterns.every((pattern) => allModelIds.has(pattern));
820
+ if (allEnabled) {
821
+ // Every enabled id resolves to a concrete model: build scope directly.
822
+ const enabledSet = new Set(patterns);
823
+ const scopedModels = allModels.filter((model) => enabledSet.has(`${model.provider}/${model.id}`));
824
+ session.setScopedModels(scopedModels.map((model) => ({ model })));
825
+ return success(id, "set_session_models", {
826
+ enabled: scopedModels.map((model) => `${model.provider}/${model.id}`),
827
+ models: scopedModels,
828
+ });
829
+ }
830
+ // Patterns contain a scope/wildcard (e.g. "anthropic/*"): resolve against the registry.
831
+ const scoped = await resolveModelScope(patterns, session.modelRuntime);
832
+ session.setScopedModels(scoped.map((sm) => ({ model: sm.model, thinkingLevel: sm.thinkingLevel })));
833
+ return success(id, "set_session_models", {
834
+ enabled: scoped.map((sm) => `${sm.model.provider}/${sm.model.id}`),
835
+ models: scoped.map((sm) => sm.model),
836
+ });
837
+ }
838
+ catch (scopeError) {
839
+ return error(id, "set_session_models", scopeError instanceof Error ? scopeError.message : String(scopeError));
840
+ }
841
+ }
842
+ // =================================================================
594
843
  // Messages
595
844
  // =================================================================
596
845
  case "get_messages": {
@@ -617,8 +866,438 @@ export async function runRpcMode(runtimeHost) {
617
866
  sourceInfo: template.sourceInfo,
618
867
  });
619
868
  }
869
+ // Built-in TUI commands: display-only, cannot be invoked via prompt.
870
+ for (const builtin of BUILTIN_SLASH_COMMANDS) {
871
+ commands.push({
872
+ name: builtin.name,
873
+ description: builtin.description
874
+ + (builtin.argumentHint ? ` ${builtin.argumentHint}` : ""),
875
+ source: "builtin",
876
+ sourceInfo: createSyntheticSourceInfo(`builtin:${builtin.name}`, { source: "builtin" }),
877
+ interactiveOnly: true,
878
+ });
879
+ }
620
880
  return success(id, "get_commands", { commands });
621
881
  }
882
+ // =================================================================
883
+ // Settings
884
+ // =================================================================
885
+ case "get_settings": {
886
+ const scope = command.scope ?? "effective";
887
+ const settings = scope === "global"
888
+ ? session.settingsManager.getGlobalSettings()
889
+ : scope === "project"
890
+ ? session.settingsManager.getProjectSettings()
891
+ : session.settingsManager.getEffectiveSettings();
892
+ return success(id, "get_settings", { scope, settings });
893
+ }
894
+ case "set_settings": {
895
+ const scope = command.scope ?? "global";
896
+ try {
897
+ if (scope === "project") {
898
+ session.settingsManager.setProjectSettings(command.values);
899
+ }
900
+ else {
901
+ session.settingsManager.setGlobalSettings(command.values);
902
+ }
903
+ await session.settingsManager.flush();
904
+ return success(id, "set_settings", { scope, values: structuredClone(command.values) });
905
+ }
906
+ catch (writeError) {
907
+ return error(id, "set_settings", writeError instanceof Error ? writeError.message : String(writeError));
908
+ }
909
+ }
910
+ case "factory_reset_settings": {
911
+ if (session.isStreaming) {
912
+ return error(id, "factory_reset_settings", "Wait for the current response to finish before resetting settings.");
913
+ }
914
+ if (session.isCompacting) {
915
+ return error(id, "factory_reset_settings", "Wait for compaction to finish before resetting settings.");
916
+ }
917
+ try {
918
+ const agentDir = getAgentDir();
919
+ const settingsPath = join(agentDir, "settings.json");
920
+ const bundledPath = join(getBundledDefaultsDir(), "settings.json");
921
+ if (!existsSync(bundledPath)) {
922
+ return error(id, "factory_reset_settings", `Bundled defaults not found at ${bundledPath}`);
923
+ }
924
+ if (existsSync(settingsPath)) {
925
+ copyFileSync(settingsPath, `${settingsPath}.bak`);
926
+ }
927
+ copyFileSync(bundledPath, settingsPath);
928
+ try {
929
+ chmodSync(settingsPath, 0o600);
930
+ }
931
+ catch {
932
+ // best-effort
933
+ }
934
+ await session.reload();
935
+ return success(id, "factory_reset_settings");
936
+ }
937
+ catch (resetError) {
938
+ return error(id, "factory_reset_settings", resetError instanceof Error ? resetError.message : String(resetError));
939
+ }
940
+ }
941
+ // =================================================================
942
+ // Auth
943
+ // =================================================================
944
+ case "get_auth_providers": {
945
+ const providers = session.modelRuntime
946
+ .getProviders()
947
+ .map((provider) => {
948
+ const authTypes = [];
949
+ if (provider.auth.oauth)
950
+ authTypes.push("oauth");
951
+ if (provider.auth.apiKey)
952
+ authTypes.push("api_key");
953
+ return { providerId: provider.id, name: provider.name, authTypes };
954
+ })
955
+ .sort((a, b) => a.name.localeCompare(b.name));
956
+ return success(id, "get_auth_providers", { providers });
957
+ }
958
+ case "login": {
959
+ const provider = session.modelRuntime.getProvider(command.providerId);
960
+ if (!provider) {
961
+ return error(id, "login", `Unknown provider: ${command.providerId}`);
962
+ }
963
+ if (command.authType === "oauth" && !provider.auth.oauth) {
964
+ return error(id, "login", `Provider ${command.providerId} does not support OAuth login`);
965
+ }
966
+ if (command.authType === "api_key" && !provider.auth.apiKey) {
967
+ return error(id, "login", `Provider ${command.providerId} does not support API key login`);
968
+ }
969
+ try {
970
+ const abortController = new AbortController();
971
+ await session.modelRuntime.login(command.providerId, command.authType, {
972
+ signal: abortController.signal,
973
+ prompt: async (prompt) => {
974
+ if (prompt.signal?.aborted)
975
+ throw new Error("Login cancelled");
976
+ const id = crypto.randomUUID();
977
+ const result = await new Promise((resolve, reject) => {
978
+ const onAbort = () => {
979
+ cleanup();
980
+ resolve({ type: "extension_ui_response", id, cancelled: true });
981
+ };
982
+ const cleanup = () => {
983
+ prompt.signal?.removeEventListener("abort", onAbort);
984
+ pendingExtensionRequests.delete(id);
985
+ };
986
+ prompt.signal?.addEventListener("abort", onAbort, { once: true });
987
+ pendingExtensionRequests.set(id, {
988
+ resolve: (response) => {
989
+ cleanup();
990
+ resolve(response);
991
+ },
992
+ reject: (err) => {
993
+ cleanup();
994
+ reject(err);
995
+ },
996
+ });
997
+ const request = {
998
+ type: "extension_ui_request",
999
+ id,
1000
+ method: prompt.type === "select" ? "select" : "input",
1001
+ title: prompt.message,
1002
+ };
1003
+ if (prompt.type === "select") {
1004
+ request.options = prompt.options.map((option) => option.description ? `${option.label} (${option.description})` : option.label);
1005
+ }
1006
+ else {
1007
+ if (prompt.placeholder)
1008
+ request.placeholder = prompt.placeholder;
1009
+ if (prompt.type === "secret")
1010
+ request.inputKind = "secret";
1011
+ }
1012
+ output(request);
1013
+ });
1014
+ if (result.type === "extension_ui_response" && "cancelled" in result && result.cancelled) {
1015
+ throw new Error("Login cancelled");
1016
+ }
1017
+ if ("value" in result)
1018
+ return result.value;
1019
+ if ("values" in result)
1020
+ return (result.values ? result.values[0] : undefined);
1021
+ return undefined;
1022
+ },
1023
+ notify: (event) => {
1024
+ if (event.type === "auth_url") {
1025
+ openBrowser(event.url);
1026
+ output({
1027
+ type: "extension_ui_request",
1028
+ id: crypto.randomUUID(),
1029
+ method: "notify",
1030
+ message: event.instructions
1031
+ ? `Open in your browser: ${event.url}\n${event.instructions}`
1032
+ : `Open in your browser: ${event.url}`,
1033
+ notifyType: "info",
1034
+ });
1035
+ }
1036
+ else if (event.type === "device_code") {
1037
+ openBrowser(event.verificationUri);
1038
+ output({
1039
+ type: "extension_ui_request",
1040
+ id: crypto.randomUUID(),
1041
+ method: "notify",
1042
+ message: `Open ${event.verificationUri} and enter code: ${event.userCode}`,
1043
+ notifyType: "info",
1044
+ });
1045
+ }
1046
+ else if (event.type === "info") {
1047
+ output({
1048
+ type: "extension_ui_request",
1049
+ id: crypto.randomUUID(),
1050
+ method: "notify",
1051
+ message: event.message,
1052
+ notifyType: "info",
1053
+ });
1054
+ }
1055
+ else if (event.type === "progress") {
1056
+ output({
1057
+ type: "extension_ui_request",
1058
+ id: crypto.randomUUID(),
1059
+ method: "notify",
1060
+ message: event.message,
1061
+ notifyType: "info",
1062
+ });
1063
+ }
1064
+ },
1065
+ });
1066
+ const actionLabel = command.authType === "oauth" ? `Logged in to ${provider.name}` : `Saved API key for ${provider.name}`;
1067
+ const authType = command.authType;
1068
+ const isUnknownModel = session.model?.provider === "unknown" && session.model?.id === "unknown";
1069
+ if (isUnknownModel) {
1070
+ const available = await session.modelRuntime.getAvailable();
1071
+ const providerModels = available.filter((model) => model.provider === command.providerId);
1072
+ if (providerModels.length > 0) {
1073
+ await session.setModel(providerModels[0]);
1074
+ }
1075
+ }
1076
+ return success(id, "login", {
1077
+ providerId: command.providerId,
1078
+ authType,
1079
+ message: `${actionLabel}. Credentials saved.`,
1080
+ });
1081
+ }
1082
+ catch (loginError) {
1083
+ const message = loginError instanceof Error ? loginError.message : String(loginError);
1084
+ if (message === "Login cancelled") {
1085
+ return error(id, "login", "Login cancelled");
1086
+ }
1087
+ return error(id, "login", message);
1088
+ }
1089
+ }
1090
+ case "logout": {
1091
+ const provider = session.modelRuntime.getProvider(command.providerId);
1092
+ if (!provider) {
1093
+ return error(id, "logout", `Unknown provider: ${command.providerId}`);
1094
+ }
1095
+ try {
1096
+ await session.modelRuntime.logout(command.providerId);
1097
+ return success(id, "logout", { providerId: command.providerId });
1098
+ }
1099
+ catch (logoutError) {
1100
+ return error(id, "logout", logoutError instanceof Error ? logoutError.message : String(logoutError));
1101
+ }
1102
+ }
1103
+ case "get_auth_state": {
1104
+ try {
1105
+ const credentials = await session.modelRuntime.listCredentials();
1106
+ const providers = credentials.map(({ providerId, type }) => ({
1107
+ providerId,
1108
+ authType: type,
1109
+ source: "stored",
1110
+ }));
1111
+ // Environment/configured auth not in the credential store.
1112
+ for (const provider of session.modelRuntime.getProviders()) {
1113
+ if (providers.some((entry) => entry.providerId === provider.id))
1114
+ continue;
1115
+ const status = session.modelRuntime.getProviderAuthStatus(provider.id);
1116
+ if (status.configured) {
1117
+ providers.push({
1118
+ providerId: provider.id,
1119
+ authType: session.modelRuntime.isUsingOAuth(provider.id) ? "oauth" : "api_key",
1120
+ source: status.label ?? status.source ?? "environment",
1121
+ });
1122
+ }
1123
+ }
1124
+ return success(id, "get_auth_state", { providers });
1125
+ }
1126
+ catch (authStateError) {
1127
+ return error(id, "get_auth_state", authStateError instanceof Error ? authStateError.message : String(authStateError));
1128
+ }
1129
+ }
1130
+ // =================================================================
1131
+ // Scoped Models
1132
+ // =================================================================
1133
+ case "get_scoped_models": {
1134
+ const enabled = session.settingsManager.getEnabledModels() ?? [];
1135
+ const scoped = session.scopedModels.map((entry) => entry.model);
1136
+ const models = scoped.length > 0 ? scoped : await session.modelRuntime.getAvailable();
1137
+ return success(id, "get_scoped_models", { enabled, models: [...models] });
1138
+ }
1139
+ case "set_scoped_models": {
1140
+ if (command.enabled !== undefined) {
1141
+ session.settingsManager.setEnabledModels(command.enabled.length > 0 ? command.enabled : undefined);
1142
+ }
1143
+ const enabled = session.settingsManager.getEnabledModels() ?? [];
1144
+ if (command.reorder !== undefined && command.reorder.length > 0) {
1145
+ const patternSet = new Set(command.reorder);
1146
+ const kept = enabled.filter((pattern) => patternSet.has(pattern));
1147
+ const added = command.reorder.filter((pattern) => !kept.includes(pattern));
1148
+ session.settingsManager.setEnabledModels([...kept, ...added]);
1149
+ }
1150
+ await session.settingsManager.flush();
1151
+ return success(id, "set_scoped_models", { enabled: session.settingsManager.getEnabledModels() ?? [] });
1152
+ }
1153
+ // =================================================================
1154
+ // Session Import
1155
+ // =================================================================
1156
+ case "import_jsonl": {
1157
+ try {
1158
+ await runtimeHost.importFromJsonl(command.path);
1159
+ await rebindSession();
1160
+ const sessionFile = session.sessionFile;
1161
+ return success(id, "import_jsonl", { sessionPath: sessionFile ?? command.path });
1162
+ }
1163
+ catch (importError) {
1164
+ return error(id, "import_jsonl", importError instanceof Error ? importError.message : String(importError));
1165
+ }
1166
+ }
1167
+ // =================================================================
1168
+ // Git helper
1169
+ // =================================================================
1170
+ case "git": {
1171
+ const prompt = buildGitCommandPrompt(command.command);
1172
+ let gitPreflightSucceeded = false;
1173
+ void session
1174
+ .prompt(prompt, {
1175
+ streamingBehavior: "steer",
1176
+ source: "rpc",
1177
+ preflightResult: (didSucceed) => {
1178
+ if (didSucceed) {
1179
+ gitPreflightSucceeded = true;
1180
+ output(success(id, "git"));
1181
+ }
1182
+ },
1183
+ })
1184
+ .catch((e) => {
1185
+ if (!gitPreflightSucceeded) {
1186
+ output(error(id, "git", e.message));
1187
+ }
1188
+ });
1189
+ return undefined;
1190
+ }
1191
+ // =================================================================
1192
+ // Reload
1193
+ // =================================================================
1194
+ case "reload": {
1195
+ if (session.isStreaming) {
1196
+ return error(id, "reload", "Wait for the current response to finish before reloading.");
1197
+ }
1198
+ if (session.isCompacting) {
1199
+ return error(id, "reload", "Wait for compaction to finish before reloading.");
1200
+ }
1201
+ try {
1202
+ await session.reload();
1203
+ return success(id, "reload");
1204
+ }
1205
+ catch (reloadError) {
1206
+ return error(id, "reload", reloadError instanceof Error ? reloadError.message : String(reloadError));
1207
+ }
1208
+ }
1209
+ // =================================================================
1210
+ // Tree ops
1211
+ // =================================================================
1212
+ case "set_entry_label": {
1213
+ try {
1214
+ const labelId = session.sessionManager.appendLabelChange(command.entryId, command.label);
1215
+ return success(id, "set_entry_label", { labelId });
1216
+ }
1217
+ catch (labelError) {
1218
+ return error(id, "set_entry_label", labelError instanceof Error ? labelError.message : String(labelError));
1219
+ }
1220
+ }
1221
+ // =================================================================
1222
+ // Trust
1223
+ // =================================================================
1224
+ case "get_trust": {
1225
+ const cwd = session.sessionManager.getCwd();
1226
+ const trustStore = new ProjectTrustStore(runtimeHost.services.agentDir);
1227
+ const entry = trustStore.getEntry(cwd);
1228
+ return success(id, "get_trust", {
1229
+ cwd,
1230
+ savedDecision: entry ? { path: entry.path, decision: entry.decision } : null,
1231
+ projectTrusted: session.settingsManager.isProjectTrusted(),
1232
+ trustRequired: hasTrustRequiringProjectResources(cwd),
1233
+ });
1234
+ }
1235
+ case "set_trust": {
1236
+ const cwd = session.sessionManager.getCwd();
1237
+ const trustStore = new ProjectTrustStore(runtimeHost.services.agentDir);
1238
+ try {
1239
+ if (command.decision === null) {
1240
+ trustStore.set(cwd, null);
1241
+ }
1242
+ else {
1243
+ trustStore.set(cwd, command.decision);
1244
+ }
1245
+ return success(id, "set_trust", { decision: command.decision, restartRequired: command.decision !== null });
1246
+ }
1247
+ catch (trustError) {
1248
+ return error(id, "set_trust", trustError instanceof Error ? trustError.message : String(trustError));
1249
+ }
1250
+ }
1251
+ // =================================================================
1252
+ // Hotkeys / version / share
1253
+ // =================================================================
1254
+ case "get_hotkeys": {
1255
+ const keybindings = KeybindingsManager.create();
1256
+ const hotkeys = keybindings.getEffectiveConfig();
1257
+ return success(id, "get_hotkeys", { hotkeys });
1258
+ }
1259
+ case "get_available_themes": {
1260
+ // Mirror TUI startup: register extension/package themes before listing.
1261
+ setRegisteredThemes(session.resourceLoader.getThemes().themes);
1262
+ const themes = getAvailableThemesWithPaths().map(({ name, path }) => ({ name, path }));
1263
+ const currentSetting = session.settingsManager.getThemeSetting();
1264
+ const current = currentSetting?.includes("/") ? undefined : currentSetting;
1265
+ return success(id, "get_available_themes", { themes, current });
1266
+ }
1267
+ case "get_version_info": {
1268
+ const changelog = parseChangelog(getChangelogPath());
1269
+ return success(id, "get_version_info", { version: VERSION, changelog });
1270
+ }
1271
+ case "share_gist": {
1272
+ try {
1273
+ const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
1274
+ if (authResult.status !== 0) {
1275
+ return error(id, "share_gist", "GitHub CLI is not logged in. Run 'gh auth login' first.");
1276
+ }
1277
+ const tmpFile = join(tmpdir(), `selesai-session-${crypto.randomUUID()}.html`);
1278
+ await session.exportToHtml(tmpFile, { themeName: command.themeName });
1279
+ const result = spawnSync("gh", ["gist", "create", "--public=false", tmpFile], { encoding: "utf-8" });
1280
+ try {
1281
+ unlinkSync(tmpFile);
1282
+ }
1283
+ catch {
1284
+ // best-effort cleanup
1285
+ }
1286
+ if (result.status !== 0) {
1287
+ return error(id, "share_gist", (result.stderr ?? "Unknown error").trim() || "Failed to create gist");
1288
+ }
1289
+ const gistUrl = (result.stdout ?? "").trim();
1290
+ const gistId = gistUrl.split("/").pop();
1291
+ if (!gistId) {
1292
+ return error(id, "share_gist", "Failed to parse gist ID from gh output");
1293
+ }
1294
+ const previewUrl = getShareViewerUrl(gistId);
1295
+ return success(id, "share_gist", { url: previewUrl, gistUrl });
1296
+ }
1297
+ catch (shareError) {
1298
+ return error(id, "share_gist", shareError instanceof Error ? shareError.message : String(shareError));
1299
+ }
1300
+ }
622
1301
  default: {
623
1302
  const unknownCommand = command;
624
1303
  return error(id, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);