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.
- package/dist/bundle/wave.mjs +1075 -613
- package/package.json +4 -3
- package/src/cli.tsx +9 -0
- package/src/commands/plugin/install.ts +6 -7
- package/src/commands/plugin/uninstall.ts +11 -5
- package/src/components/ChatInterface.tsx +3 -0
- package/src/components/ConfirmationDetails.tsx +36 -15
- package/src/components/ConfirmationSelector.tsx +39 -0
- package/src/components/HooksManager.tsx +286 -0
- package/src/components/InputBox.tsx +125 -51
- package/src/components/Markdown.tsx +21 -8
- package/src/components/MarketplaceDetail.tsx +10 -27
- package/src/components/MarketplaceView.tsx +17 -3
- package/src/components/MessageBlockItem.tsx +0 -5
- package/src/components/MessageList.tsx +0 -1
- package/src/components/Notifications.tsx +7 -0
- package/src/components/PlanView.tsx +146 -0
- package/src/components/PluginManagerShell.tsx +3 -3
- package/src/components/PluginManagerTypes.ts +2 -1
- package/src/components/ResumeCommand.tsx +84 -0
- package/src/components/ScrolledContent.tsx +71 -0
- package/src/components/SessionSelector.tsx +184 -6
- package/src/components/SessionSelectorFlow.tsx +246 -0
- package/src/constants/commands.ts +30 -0
- package/src/contexts/useChat.tsx +252 -98
- package/src/daemon/commands.ts +1046 -67
- package/src/daemon-cli.ts +6 -0
- package/src/hooks/useInputManager.ts +30 -0
- package/src/hooks/useLineScroll.ts +58 -0
- package/src/hooks/usePluginManager.ts +104 -78
- package/src/index.ts +156 -16
- package/src/managers/inputHandlers.ts +16 -9
- package/src/managers/inputReducer.ts +39 -9
- package/src/reducers/confirmationReducer.ts +20 -1
- package/src/reducers/hooksManagerReducer.ts +92 -0
- package/src/reducers/selectorReducer.ts +3 -0
- package/src/session-selector-cli.tsx +9 -255
- package/src/stdio/agentBridge.ts +932 -133
- package/src/stdio/daemonServer.ts +26 -82
- package/src/stdio/protocol.ts +23 -4
- package/src/stdio-cli.ts +60 -0
- package/src/utils/rewindCheckpoints.ts +2 -2
- package/src/utils/sessionOwnership.ts +62 -0
- package/src/utils/worktree.ts +175 -50
- package/src/components/BangDisplay.tsx +0 -41
- 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",
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -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
|
-
//
|
|
152
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
1486
|
+
reloadPlugins,
|
|
1487
|
+
notifyPluginChange,
|
|
1488
|
+
pluginHint,
|
|
1335
1489
|
};
|
|
1336
1490
|
|
|
1337
1491
|
return (
|