wave-code 1.1.5 → 1.3.0

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 (46) hide show
  1. package/dist/bundle/wave.mjs +1075 -613
  2. package/package.json +4 -3
  3. package/src/cli.tsx +9 -0
  4. package/src/commands/plugin/install.ts +6 -7
  5. package/src/commands/plugin/uninstall.ts +11 -5
  6. package/src/components/ChatInterface.tsx +3 -0
  7. package/src/components/ConfirmationDetails.tsx +36 -15
  8. package/src/components/ConfirmationSelector.tsx +39 -0
  9. package/src/components/HooksManager.tsx +286 -0
  10. package/src/components/InputBox.tsx +125 -51
  11. package/src/components/Markdown.tsx +21 -8
  12. package/src/components/MarketplaceDetail.tsx +10 -27
  13. package/src/components/MarketplaceView.tsx +17 -3
  14. package/src/components/MessageBlockItem.tsx +0 -5
  15. package/src/components/MessageList.tsx +0 -1
  16. package/src/components/Notifications.tsx +7 -0
  17. package/src/components/PlanView.tsx +146 -0
  18. package/src/components/PluginManagerShell.tsx +3 -3
  19. package/src/components/PluginManagerTypes.ts +2 -1
  20. package/src/components/ResumeCommand.tsx +84 -0
  21. package/src/components/ScrolledContent.tsx +71 -0
  22. package/src/components/SessionSelector.tsx +184 -6
  23. package/src/components/SessionSelectorFlow.tsx +246 -0
  24. package/src/constants/commands.ts +30 -0
  25. package/src/contexts/useChat.tsx +252 -98
  26. package/src/daemon/commands.ts +1046 -67
  27. package/src/daemon-cli.ts +6 -0
  28. package/src/hooks/useInputManager.ts +30 -0
  29. package/src/hooks/useLineScroll.ts +58 -0
  30. package/src/hooks/usePluginManager.ts +104 -78
  31. package/src/index.ts +156 -16
  32. package/src/managers/inputHandlers.ts +16 -9
  33. package/src/managers/inputReducer.ts +39 -9
  34. package/src/reducers/confirmationReducer.ts +20 -1
  35. package/src/reducers/hooksManagerReducer.ts +92 -0
  36. package/src/reducers/selectorReducer.ts +3 -0
  37. package/src/session-selector-cli.tsx +9 -255
  38. package/src/stdio/agentBridge.ts +932 -133
  39. package/src/stdio/daemonServer.ts +26 -82
  40. package/src/stdio/protocol.ts +23 -4
  41. package/src/stdio-cli.ts +60 -0
  42. package/src/utils/rewindCheckpoints.ts +2 -2
  43. package/src/utils/sessionOwnership.ts +62 -0
  44. package/src/utils/worktree.ts +175 -50
  45. package/src/components/BangDisplay.tsx +0 -41
  46. package/src/components/PlanDisplay.tsx +0 -18
@@ -25,6 +25,12 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
25
25
  description: "List available skills",
26
26
  handler: () => {}, // Handler here won't be used, actual processing is in the hook
27
27
  },
28
+ {
29
+ id: "hooks",
30
+ name: "hooks",
31
+ description: "View configured hooks",
32
+ handler: () => {}, // Handler here won't be used, actual processing is in the hook
33
+ },
28
34
  {
29
35
  id: "rewind",
30
36
  name: "rewind",
@@ -50,6 +56,12 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
50
56
  description: "View and manage plugins",
51
57
  handler: () => {}, // Handler here won't be used, actual processing is in the hook
52
58
  },
59
+ {
60
+ id: "reload-plugins",
61
+ name: "reload-plugins",
62
+ description: "Reload plugins in place, without restarting the session",
63
+ handler: () => {}, // Handler here won't be used, actual processing is in the hook
64
+ },
53
65
  {
54
66
  id: "btw",
55
67
  name: "btw",
@@ -80,6 +92,24 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
80
92
  description: "View and manage workflow runs",
81
93
  handler: () => {}, // Handler here won't be used, actual processing is in the hook
82
94
  },
95
+ {
96
+ id: "plan",
97
+ name: "plan",
98
+ description: "Enable plan mode or view the current session plan",
99
+ handler: () => {}, // Handler here won't be used, actual processing is in the hook
100
+ },
101
+ {
102
+ id: "resume",
103
+ name: "resume",
104
+ description: "Switch to another conversation without leaving the session",
105
+ handler: () => {}, // Handler here won't be used, actual processing is in the hook
106
+ },
107
+ {
108
+ id: "rename",
109
+ name: "rename",
110
+ description: "Set a custom title for the current conversation",
111
+ handler: () => {}, // Handler here won't be used, actual processing is in the hook
112
+ },
83
113
  {
84
114
  id: "clear",
85
115
  name: "clear",
@@ -17,6 +17,7 @@ import type {
17
17
  SlashCommand,
18
18
  SubagentConfiguration,
19
19
  SkillMetadata,
20
+ HookEventConfig,
20
21
  PermissionDecision,
21
22
  PermissionMode,
22
23
  QueuedMessage,
@@ -30,14 +31,23 @@ import {
30
31
  type WorktreeSession,
31
32
  OPERATION_CANCELLED_BY_USER,
32
33
  extractLatestTotalTokens,
34
+ PLUGIN_CHANGE_PENDING_MESSAGE,
35
+ PLUGIN_RELOADED_MESSAGE,
33
36
  } from "wave-agent-sdk";
34
37
  import { logger } from "../utils/logger.js";
35
38
  import { displayUsageSummary } from "../utils/usageSummary.js";
36
39
  import { expandLongTextPlaceholders } from "../managers/inputHandlers.js";
40
+ import { readFile } from "node:fs/promises";
37
41
 
38
42
  import { BaseAppProps } from "../types.js";
39
43
 
40
44
  // Main Chat Context
45
+ export interface PlanViewData {
46
+ path?: string;
47
+ content?: string;
48
+ message?: string;
49
+ }
50
+
41
51
  export interface ChatContextType {
42
52
  messages: Message[];
43
53
  isLoading: boolean;
@@ -69,6 +79,17 @@ export interface ChatContextType {
69
79
  clearMessages: () => Promise<void>;
70
80
  compact: (instructions?: string) => Promise<void>;
71
81
  addDir: (args?: string) => Promise<void>;
82
+ /**
83
+ * Set the current session's custom title (`/rename <title>`). A blank title
84
+ * only prints the usage hint — no AI-generated titles (spec: session-management.md).
85
+ */
86
+ renameSession: (args?: string) => Promise<void>;
87
+ /**
88
+ * Switch to another conversation in-process (`/resume`). Pass `resumeWorkdir`
89
+ * when the target session lives in a sibling worktree of the same repo — the
90
+ * session then moves into that directory too.
91
+ */
92
+ resumeSession: (sessionId: string, resumeWorkdir?: string) => Promise<void>;
72
93
  abortMessage: () => void;
73
94
  recallQueuedMessage: () => QueuedMessage | null;
74
95
  removeQueuedMessageById: (id: string) => boolean;
@@ -104,9 +125,17 @@ export interface ChatContextType {
104
125
  agentDefinitions: SubagentConfiguration[];
105
126
  // Skill metadata (for /skills overlay)
106
127
  skills: SkillMetadata[];
128
+ // Hook configs per scope (for /hooks overlay)
129
+ hooks: Partial<
130
+ Record<"user" | "project" | "plugin", Record<string, HookEventConfig[]>>
131
+ >;
107
132
  // Permission functionality
108
133
  permissionMode: PermissionMode;
109
134
  setPermissionMode: (mode: PermissionMode) => void;
135
+ // /plan overlay state + handler
136
+ planView: PlanViewData | null;
137
+ setPlanView: (view: PlanViewData | null) => void;
138
+ handlePlanCommand: (args?: string) => Promise<void>;
110
139
  // Permission confirmation state
111
140
  isConfirmationVisible: boolean;
112
141
  hasPendingConfirmations: boolean;
@@ -115,6 +144,7 @@ export interface ChatContextType {
115
144
  input?: Record<string, unknown>;
116
145
  suggestedPrefix?: string;
117
146
  hidePersistentOption?: boolean;
147
+ outsideSafeZoneDirectory?: string;
118
148
  planContent?: string;
119
149
  permissionMode?: PermissionMode;
120
150
  warning?: string;
@@ -127,6 +157,7 @@ export interface ChatContextType {
127
157
  planContent?: string,
128
158
  permissionMode?: PermissionMode,
129
159
  warning?: string,
160
+ outsideSafeZoneDirectory?: string,
130
161
  ) => Promise<PermissionDecision>;
131
162
  hideConfirmation: () => void;
132
163
  handleConfirmationDecision: (decision: PermissionDecision) => void;
@@ -148,8 +179,10 @@ export interface ChatContextType {
148
179
  workingDirectory: string;
149
180
  version?: string;
150
181
  workdir?: string;
151
- // Agent recreation (e.g. after plugin install)
152
- recreateAgent: () => void;
182
+ // Plugin in-place reload (docs/specs/ecosystem/plugin.md「插件变更的就地重载」)
183
+ reloadPlugins: () => Promise<void>;
184
+ notifyPluginChange: () => void;
185
+ pluginHint: string | null;
153
186
  }
154
187
 
155
188
  const ChatContext = createContext<ChatContextType | null>(null);
@@ -472,6 +505,12 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
472
505
  >([]);
473
506
  // Skill metadata (for /skills overlay)
474
507
  const [skills, setSkills] = useState<SkillMetadata[]>([]);
508
+ // Hook configs per scope (for /hooks overlay)
509
+ const [hooks, setHooks] = useState<
510
+ Partial<
511
+ Record<"user" | "project" | "plugin", Record<string, HookEventConfig[]>>
512
+ >
513
+ >({});
475
514
 
476
515
  // Permission state
477
516
  const [permissionMode, setPermissionModeState] = useState<PermissionMode>(
@@ -479,6 +518,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
479
518
  (bypassPermissions ? "bypassPermissions" : "default"),
480
519
  );
481
520
 
521
+ // Plan view state for the /plan command overlay
522
+ const [planView, setPlanView] = useState<PlanViewData | null>(null);
523
+
482
524
  // Confirmation state with queue-based architecture
483
525
  const [isConfirmationVisible, setIsConfirmationVisible] = useState(false);
484
526
  const [confirmingTool, setConfirmingTool] = useState<
@@ -487,6 +529,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
487
529
  input?: Record<string, unknown>;
488
530
  suggestedPrefix?: string;
489
531
  hidePersistentOption?: boolean;
532
+ outsideSafeZoneDirectory?: string;
490
533
  planContent?: string;
491
534
  permissionMode?: PermissionMode;
492
535
  warning?: string;
@@ -499,6 +542,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
499
542
  toolInput?: Record<string, unknown>;
500
543
  suggestedPrefix?: string;
501
544
  hidePersistentOption?: boolean;
545
+ outsideSafeZoneDirectory?: string;
502
546
  planContent?: string;
503
547
  permissionMode?: PermissionMode;
504
548
  warning?: string;
@@ -511,6 +555,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
511
555
  toolInput?: Record<string, unknown>;
512
556
  suggestedPrefix?: string;
513
557
  hidePersistentOption?: boolean;
558
+ outsideSafeZoneDirectory?: string;
514
559
  planContent?: string;
515
560
  permissionMode?: PermissionMode;
516
561
  warning?: string;
@@ -540,7 +585,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
540
585
  // the incremental callbacks in initializeAgent below.
541
586
  const refreshMessages = useCallback(() => {
542
587
  if (!isExpandedRef.current && agentRef.current) {
543
- const msgs = agentRef.current.messages.map(snapshotMessage);
588
+ // Pull the full UI display stream (keeps pre-compaction history);
589
+ // `agent.messages` would only expose the folded API context.
590
+ const msgs = agentRef.current.displayMessages.map(snapshotMessage);
544
591
  // Snapshot-safe: the full-list replacement makes the SDK state
545
592
  // authoritative. Any update still queued inside the 500ms throttle
546
593
  // window was applied to the SDK before this pull, so it is already
@@ -565,6 +612,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
565
612
  planContent?: string,
566
613
  permissionMode?: PermissionMode,
567
614
  warning?: string,
615
+ outsideSafeZoneDirectory?: string,
568
616
  ): Promise<PermissionDecision> => {
569
617
  return new Promise<PermissionDecision>((resolve, reject) => {
570
618
  const queueItem = {
@@ -572,6 +620,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
572
620
  toolInput,
573
621
  suggestedPrefix,
574
622
  hidePersistentOption,
623
+ outsideSafeZoneDirectory,
575
624
  planContent,
576
625
  permissionMode,
577
626
  warning,
@@ -674,73 +723,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
674
723
  // delayed by a streaming window).
675
724
  updateMessages.flush();
676
725
  },
677
- onAddBangMessage: (command, messageId) => {
678
- if (isExpandedRef.current) return;
679
- updateMessages((prev) =>
680
- prev.some((m) => m.id === messageId)
681
- ? prev
682
- : [
683
- ...prev,
684
- {
685
- id: messageId,
686
- role: "user",
687
- timestamp: new Date().toISOString(),
688
- blocks: [
689
- {
690
- type: "bang",
691
- command,
692
- output: "",
693
- stage: "running",
694
- exitCode: null,
695
- },
696
- ],
697
- },
698
- ],
699
- );
700
- updateMessages.flush();
701
- },
702
- onUpdateBangMessage: (command, output, messageId) => {
703
- if (isExpandedRef.current) return;
704
- updateMessages((prev) =>
705
- prev.map((m) =>
706
- m.id === messageId
707
- ? {
708
- ...m,
709
- blocks: m.blocks.map((b, idx) =>
710
- idx === m.blocks.length - 1 && b.type === "bang"
711
- ? { ...b, command, output }
712
- : b,
713
- ),
714
- }
715
- : m,
716
- ),
717
- );
718
- },
719
- onCompleteBangMessage: (command, exitCode, messageId, output) => {
720
- if (isExpandedRef.current) return;
721
- updateMessages((prev) =>
722
- prev.map((m) =>
723
- m.id === messageId
724
- ? {
725
- ...m,
726
- blocks: m.blocks.map((b, idx) =>
727
- idx === m.blocks.length - 1 && b.type === "bang"
728
- ? {
729
- ...b,
730
- command,
731
- exitCode,
732
- stage: "end",
733
- ...(output !== undefined ? { output } : {}),
734
- }
735
- : b,
736
- ),
737
- }
738
- : m,
739
- ),
740
- );
741
- // Completion signal — flush so the final state applies immediately.
742
- updateMessages.flush();
743
- },
744
726
  onLatestTotalTokensChange: (tokens) => {
745
727
  setLatestTotalTokens(tokens);
746
728
  },
@@ -815,6 +797,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
815
797
  context.planContent,
816
798
  context.permissionMode,
817
799
  context.warning,
800
+ context.outsideSafeZoneDirectory,
818
801
  );
819
802
  } catch {
820
803
  // If confirmation was cancelled or failed, deny the operation
@@ -865,9 +848,10 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
865
848
  }
866
849
 
867
850
  // Get initial state — snapshot the SDK messages (never hold live
868
- // references; see snapshotMessage)
851
+ // references; see snapshotMessage). Uses the full display stream so a
852
+ // restored session shows pre-compaction history too.
869
853
  setSessionId(agent.sessionId);
870
- setMessages(agent.messages.map(snapshotMessage));
854
+ setMessages(agent.displayMessages.map(snapshotMessage));
871
855
  setIsLoading(agent.isLoading);
872
856
  setLatestTotalTokens(extractLatestTotalTokens(agent.messages));
873
857
  setIsCommandRunning(agent.isCommandRunning);
@@ -894,6 +878,19 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
894
878
  // Get initial skill metadata
895
879
  const initialSkills = agent.getSkillMetadata?.() || [];
896
880
  setSkills(initialSkills);
881
+
882
+ // Get initial hooks (user/project from settings.json, plugin from
883
+ // the hook manager's plugin-registered set) — snapshot for /hooks.
884
+ const [userHooks, projectHooks, pluginHooks] = await Promise.all([
885
+ agent.getHooksByScope("user"),
886
+ agent.getHooksByScope("project"),
887
+ agent.getHooksByScope("plugin"),
888
+ ]);
889
+ setHooks({
890
+ user: userHooks,
891
+ project: projectHooks,
892
+ plugin: pluginHooks,
893
+ });
897
894
  } catch (error) {
898
895
  console.error("Failed to initialize AI manager:", error);
899
896
  }
@@ -918,32 +915,50 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
918
915
  ],
919
916
  );
920
917
 
921
- // Recreate agent (e.g. after plugin install) — destroys current agent and reinitializes
922
- const recreateAgent = useCallback(() => {
923
- const currentSessionId = agentRef.current?.sessionId;
924
- if (agentRef.current) {
925
- try {
926
- agentRef.current.destroy();
927
- } catch {
928
- // Ignore destroy errors
929
- }
918
+ // 插件变更落盘后只置一个「待应用」信号并提示一次,不再重建 Agent;真正换装由
919
+ // 用户敲 /reload-plugins 触发,在当前会话内完成(docs/specs/ecosystem/plugin.md
920
+ // 「插件变更的就地重载」:会…不重建、不打断回合、不新建会话)。
921
+ const [pluginHint, setPluginHint] = useState<string | null>(null);
922
+ const pluginHintTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
923
+
924
+ const showPluginHint = useCallback((message: string) => {
925
+ setPluginHint(message);
926
+ if (pluginHintTimer.current) {
927
+ clearTimeout(pluginHintTimer.current);
930
928
  }
931
- agentRef.current = null;
932
- setMessages([]);
933
- setMcpServerStatuses([]);
934
- setSlashCommands([]);
935
- setAgentDefinitions([]);
936
- setSkills([]);
937
- setSessionId("");
938
- setIsLoading(false);
939
- setLatestTotalTokens(0);
940
- setMaxInputTokens(200000);
941
- setIsCommandRunning(false);
942
- setIsCompacting(false);
943
- if (currentSessionId) {
944
- initializeAgent(currentSessionId);
929
+ // 一次性提示:数秒后自行消失,不留常驻标识(spec「插件变更提示」)。
930
+ pluginHintTimer.current = setTimeout(() => setPluginHint(null), 8000);
931
+ }, []);
932
+
933
+ const notifyPluginChange = useCallback(() => {
934
+ showPluginHint(PLUGIN_CHANGE_PENDING_MESSAGE);
935
+ }, [showPluginHint]);
936
+
937
+ const reloadPlugins = useCallback(async () => {
938
+ const agent = agentRef.current;
939
+ if (!agent) return;
940
+ try {
941
+ const result = await agent.reloadPlugins();
942
+ // 换装后本进程各会话的能力已变,UI 侧的命令 / 子代理 / 技能列表同步重读。
943
+ setSlashCommands(agent.getSlashCommands?.() || []);
944
+ setAgentDefinitions(agent.getSubagentConfigurations?.() || []);
945
+ setSkills(agent.getSkillMetadata?.() || []);
946
+ if (result.failures.length > 0) {
947
+ // 失败不回滚:如实提示,会话继续可用(spec 场景 11)。
948
+ showPluginHint(
949
+ `${PLUGIN_RELOADED_MESSAGE}部分插件加载失败:${result.failures
950
+ .map((failure) => `${failure.path}: ${failure.error}`)
951
+ .join(";")}`,
952
+ );
953
+ } else {
954
+ showPluginHint(PLUGIN_RELOADED_MESSAGE);
955
+ }
956
+ } catch (error) {
957
+ showPluginHint(
958
+ `插件重载失败:${error instanceof Error ? error.message : String(error)}`,
959
+ );
945
960
  }
946
- }, [initializeAgent]);
961
+ }, [showPluginHint]);
947
962
 
948
963
  // Run initial agent creation
949
964
  useEffect(() => {
@@ -1087,6 +1102,90 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1087
1102
  );
1088
1103
  }, []);
1089
1104
 
1105
+ // /rename: set the current conversation's custom title. The title is stored
1106
+ // as a reserved `custom-title` entry appended to the session's own JSONL
1107
+ // file, so `wave -r` and every GUI host read the same value. A blank title
1108
+ // only prints the usage hint — no AI-generated titles (spec:
1109
+ // session-management.md "不做 AI 自动标题").
1110
+ const renameSession = useCallback(async (args?: string) => {
1111
+ const agent = agentRef.current;
1112
+ if (!agent) return;
1113
+
1114
+ const title = (args ?? "").trim();
1115
+ const showResult = (content: string) => {
1116
+ setMessages((prev) => [
1117
+ ...prev,
1118
+ {
1119
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
1120
+ role: "assistant",
1121
+ timestamp: new Date().toISOString(),
1122
+ blocks: [{ type: "text", content, stage: "end" }],
1123
+ },
1124
+ ]);
1125
+ };
1126
+
1127
+ if (!title) {
1128
+ showResult("Usage: /rename <title>");
1129
+ return;
1130
+ }
1131
+
1132
+ try {
1133
+ await agent.renameSession(title);
1134
+ showResult(`Conversation renamed to "${title}".`);
1135
+ } catch (error) {
1136
+ showResult(
1137
+ `Failed to rename conversation: ${
1138
+ error instanceof Error ? error.message : String(error)
1139
+ }`,
1140
+ );
1141
+ }
1142
+ }, []);
1143
+
1144
+ // /resume: switch to another conversation without leaving the process. The
1145
+ // session id and its on-disk project directory move together, so the next
1146
+ // message is appended to the target's own transcript. A failed restore leaves
1147
+ // the current session untouched and only surfaces a notice.
1148
+ const resumeSession = useCallback(
1149
+ async (sessionId: string, resumeWorkdir?: string) => {
1150
+ const agent = agentRef.current;
1151
+ if (!agent) return;
1152
+
1153
+ try {
1154
+ await agent.restoreSession(
1155
+ sessionId,
1156
+ resumeWorkdir ? { workdir: resumeWorkdir } : undefined,
1157
+ );
1158
+ } catch (error) {
1159
+ // UI-only notice (same display path as /add-dir): nothing was switched,
1160
+ // so nothing about the failed target is persisted.
1161
+ setMessages((prev) => [
1162
+ ...prev,
1163
+ {
1164
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
1165
+ role: "assistant",
1166
+ timestamp: new Date().toISOString(),
1167
+ blocks: [
1168
+ {
1169
+ type: "error",
1170
+ content: `Failed to resume conversation: ${
1171
+ error instanceof Error ? error.message : String(error)
1172
+ }`,
1173
+ },
1174
+ ],
1175
+ },
1176
+ ]);
1177
+ return;
1178
+ }
1179
+
1180
+ setSessionId(agent.sessionId);
1181
+ setWorkingDirectory(agent.workingDirectory);
1182
+ setIsLoading(agent.isLoading);
1183
+ refreshMessages();
1184
+ forceRemount();
1185
+ },
1186
+ [forceRemount, refreshMessages],
1187
+ );
1188
+
1090
1189
  // Unified interrupt method, interrupt both AI messages and command execution
1091
1190
  const abortMessage = useCallback(() => {
1092
1191
  agentRef.current?.abortMessage();
@@ -1111,6 +1210,52 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1111
1210
  });
1112
1211
  }, []);
1113
1212
 
1213
+ // /plan command handler:
1214
+ // - Outside plan mode: switch to plan mode, wait for the plan file path
1215
+ // (generated asynchronously by PlanManager), then either send the given
1216
+ // description as a message (starting a plan query) or show a confirmation.
1217
+ // - Inside plan mode: display the current plan file contents; `/plan open`
1218
+ // is not supported on any end (spec: plan-mode.md "三端均不支持").
1219
+ const handlePlanCommand = useCallback(
1220
+ async (args?: string) => {
1221
+ const agent = agentRef.current;
1222
+ const description = args?.trim() ?? "";
1223
+ const argList = description.split(/\s+/).filter(Boolean);
1224
+ const wantsOpen = argList[0] === "open";
1225
+
1226
+ if (permissionMode !== "plan") {
1227
+ setPermissionMode("plan");
1228
+ // Wait for the plan file path to be generated before triggering a
1229
+ // query so the model always receives the plan file location (spec:
1230
+ // plan-mode.md "路径生成必须先于查询触发").
1231
+ const planPath = await agent?.awaitPlanFilePath();
1232
+
1233
+ if (description && !wantsOpen) {
1234
+ await sendMessage(description);
1235
+ return;
1236
+ }
1237
+ setPlanView({ message: "Enabled plan mode", path: planPath });
1238
+ return;
1239
+ }
1240
+
1241
+ // Already in plan mode — display the current plan file contents.
1242
+ const planPath =
1243
+ agent?.getPlanFilePath() ?? (await agent?.awaitPlanFilePath());
1244
+ if (!planPath) {
1245
+ setPlanView({ message: "No plan written yet." });
1246
+ return;
1247
+ }
1248
+ try {
1249
+ const content = await readFile(planPath, "utf8");
1250
+ setPlanView({ path: planPath, content });
1251
+ } catch (error) {
1252
+ logger.warn("Failed to read plan file:", error);
1253
+ setPlanView({ message: "No plan written yet." });
1254
+ }
1255
+ },
1256
+ [permissionMode, sendMessage, setPermissionMode],
1257
+ );
1258
+
1114
1259
  // MCP management methods - delegate to Agent
1115
1260
  const connectMcpServer = useCallback(async (serverName: string) => {
1116
1261
  return (await agentRef.current?.connectMcpServer(serverName)) ?? false;
@@ -1150,6 +1295,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1150
1295
  input: next.toolInput,
1151
1296
  suggestedPrefix: next.suggestedPrefix,
1152
1297
  hidePersistentOption: next.hidePersistentOption,
1298
+ outsideSafeZoneDirectory: next.outsideSafeZoneDirectory,
1153
1299
  planContent: next.planContent,
1154
1300
  permissionMode: next.permissionMode,
1155
1301
  warning: next.warning,
@@ -1237,7 +1383,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1237
1383
 
1238
1384
  const setModel = useCallback((model: string) => {
1239
1385
  if (agentRef.current) {
1240
- agentRef.current.setModel(model);
1386
+ void agentRef.current.setModel(model);
1241
1387
  setCurrentModelState(model);
1242
1388
  }
1243
1389
  }, []);
@@ -1287,6 +1433,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1287
1433
  clearMessages,
1288
1434
  compact,
1289
1435
  addDir,
1436
+ renameSession,
1437
+ resumeSession,
1290
1438
  abortMessage,
1291
1439
  recallQueuedMessage,
1292
1440
  removeQueuedMessageById,
@@ -1311,8 +1459,12 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1311
1459
  hasSlashCommand,
1312
1460
  agentDefinitions,
1313
1461
  skills,
1462
+ hooks,
1314
1463
  permissionMode,
1315
1464
  setPermissionMode,
1465
+ planView,
1466
+ setPlanView,
1467
+ handlePlanCommand,
1316
1468
  isConfirmationVisible,
1317
1469
  hasPendingConfirmations: confirmationQueue.length > 0,
1318
1470
  confirmingTool,
@@ -1331,7 +1483,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1331
1483
  workingDirectory,
1332
1484
  version,
1333
1485
  workdir,
1334
- recreateAgent,
1486
+ reloadPlugins,
1487
+ notifyPluginChange,
1488
+ pluginHint,
1335
1489
  };
1336
1490
 
1337
1491
  return (