wave-code 1.1.4 → 1.2.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 +546 -525
- package/package.json +2 -2
- package/src/commands/plugin/install.ts +5 -5
- package/src/components/ChatInterface.tsx +2 -0
- package/src/components/HooksManager.tsx +286 -0
- package/src/components/InputBox.tsx +86 -49
- package/src/components/MessageBlockItem.tsx +0 -5
- package/src/components/MessageList.tsx +0 -1
- package/src/components/PlanView.tsx +141 -0
- package/src/constants/commands.ts +12 -0
- package/src/contexts/useChat.tsx +94 -70
- package/src/daemon/commands.ts +486 -30
- package/src/hooks/useInputManager.ts +14 -0
- package/src/hooks/useLineScroll.ts +58 -0
- package/src/index.ts +94 -8
- package/src/managers/inputHandlers.ts +2 -0
- package/src/managers/inputReducer.ts +10 -0
- package/src/reducers/hooksManagerReducer.ts +92 -0
- package/src/stdio/agentBridge.ts +552 -55
- package/src/stdio/daemonServer.ts +26 -82
- package/src/stdio/protocol.ts +14 -4
- package/src/stdio-cli.ts +54 -0
- package/src/utils/rewindCheckpoints.ts +2 -2
- package/src/utils/worktree.ts +175 -50
- package/src/components/BangDisplay.tsx +0 -41
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text, useInput, useWindowSize } from "ink";
|
|
3
|
+
import { useLineScroll } from "../hooks/useLineScroll.js";
|
|
4
|
+
|
|
5
|
+
// Row budget for the scroll indicators (↑ more / ↓ more, up to one each).
|
|
6
|
+
const SCROLL_INDICATOR_BUDGET = 2;
|
|
7
|
+
// Row budget for the fixed scroll-key hint shown when content is scrollable.
|
|
8
|
+
const SCROLL_HINT_BUDGET = 1;
|
|
9
|
+
// Overlay height as a fraction of the terminal height (spec: 50-60%).
|
|
10
|
+
const PLAN_OVERLAY_HEIGHT_RATIO = 0.55;
|
|
11
|
+
|
|
12
|
+
export interface PlanViewProps {
|
|
13
|
+
path?: string;
|
|
14
|
+
content?: string;
|
|
15
|
+
message?: string;
|
|
16
|
+
/** Available height (rows) for the whole overlay, computed by default from
|
|
17
|
+
* the terminal size. Tests pass it explicitly for determinism. */
|
|
18
|
+
maxHeight?: number;
|
|
19
|
+
onCancel: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Overlay shown by the /plan command:
|
|
24
|
+
* - With `message` only: a plain status line (e.g. "Enabled plan mode").
|
|
25
|
+
* - With `path`/`content`: the current plan file contents as plain text lines
|
|
26
|
+
* (no Markdown), fixed at ~55% of the terminal height with PgUp/PgDn and
|
|
27
|
+
* Ctrl+u/Ctrl+d scrolling and ↑/↓ "N more" indicators.
|
|
28
|
+
* Esc dismisses the overlay.
|
|
29
|
+
*/
|
|
30
|
+
export const PlanView: React.FC<PlanViewProps> = ({
|
|
31
|
+
path,
|
|
32
|
+
content,
|
|
33
|
+
message,
|
|
34
|
+
maxHeight,
|
|
35
|
+
onCancel,
|
|
36
|
+
}) => {
|
|
37
|
+
const { rows } = useWindowSize();
|
|
38
|
+
const overlayMaxHeight =
|
|
39
|
+
maxHeight ?? Math.max(Math.round(rows * PLAN_OVERLAY_HEIGHT_RATIO), 10);
|
|
40
|
+
|
|
41
|
+
const isMessageOnly = message !== undefined && content === undefined;
|
|
42
|
+
|
|
43
|
+
useInput((_input, key) => {
|
|
44
|
+
if (key.escape) {
|
|
45
|
+
onCancel();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Hook order must be stable across renders, so compute the scroll state
|
|
50
|
+
// before the message-only early return.
|
|
51
|
+
const headerRows = 2 + (path ? 1 : 0); // "Current Plan" + path + footer hint
|
|
52
|
+
const visibleCount = Math.max(
|
|
53
|
+
1,
|
|
54
|
+
overlayMaxHeight -
|
|
55
|
+
headerRows -
|
|
56
|
+
SCROLL_INDICATOR_BUDGET -
|
|
57
|
+
SCROLL_HINT_BUDGET,
|
|
58
|
+
);
|
|
59
|
+
const planLines =
|
|
60
|
+
!isMessageOnly && content !== undefined && content !== ""
|
|
61
|
+
? content.split("\n")
|
|
62
|
+
: [];
|
|
63
|
+
const { scrollOffset, hasMoreAbove, hasMoreBelow } = useLineScroll({
|
|
64
|
+
totalLines: planLines.length,
|
|
65
|
+
visibleCount,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (isMessageOnly) {
|
|
69
|
+
return (
|
|
70
|
+
<Box
|
|
71
|
+
flexDirection="column"
|
|
72
|
+
paddingX={1}
|
|
73
|
+
borderStyle="single"
|
|
74
|
+
borderColor="cyan"
|
|
75
|
+
borderLeft={false}
|
|
76
|
+
borderRight={false}
|
|
77
|
+
>
|
|
78
|
+
<Text color="cyan">{message}</Text>
|
|
79
|
+
<Text dimColor>Press Escape to continue</Text>
|
|
80
|
+
</Box>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const visibleLines = planLines.slice(
|
|
85
|
+
scrollOffset,
|
|
86
|
+
scrollOffset + visibleCount,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Box
|
|
91
|
+
flexDirection="column"
|
|
92
|
+
paddingX={1}
|
|
93
|
+
borderStyle="single"
|
|
94
|
+
borderColor="cyan"
|
|
95
|
+
borderLeft={false}
|
|
96
|
+
borderRight={false}
|
|
97
|
+
>
|
|
98
|
+
<Text color="cyan" bold>
|
|
99
|
+
Current Plan
|
|
100
|
+
</Text>
|
|
101
|
+
{path ? (
|
|
102
|
+
<Text dimColor wrap="truncate-end">
|
|
103
|
+
{path}
|
|
104
|
+
</Text>
|
|
105
|
+
) : null}
|
|
106
|
+
{hasMoreAbove && (
|
|
107
|
+
<Box>
|
|
108
|
+
<Text color="gray" dimColor>
|
|
109
|
+
↑ {scrollOffset} more
|
|
110
|
+
</Text>
|
|
111
|
+
</Box>
|
|
112
|
+
)}
|
|
113
|
+
{visibleLines.length > 0 ? (
|
|
114
|
+
visibleLines.map((line, index) => (
|
|
115
|
+
<Box key={scrollOffset + index}>
|
|
116
|
+
<Text>{line || " "}</Text>
|
|
117
|
+
</Box>
|
|
118
|
+
))
|
|
119
|
+
) : (
|
|
120
|
+
<Text>No plan written yet.</Text>
|
|
121
|
+
)}
|
|
122
|
+
{hasMoreBelow && (
|
|
123
|
+
<Box>
|
|
124
|
+
<Text color="gray" dimColor>
|
|
125
|
+
↓ {planLines.length - scrollOffset - visibleCount} more
|
|
126
|
+
</Text>
|
|
127
|
+
</Box>
|
|
128
|
+
)}
|
|
129
|
+
{(hasMoreAbove || hasMoreBelow) && (
|
|
130
|
+
<Box>
|
|
131
|
+
<Text color="gray" dimColor>
|
|
132
|
+
PgUp/PgDn page • Ctrl+u/d half page
|
|
133
|
+
</Text>
|
|
134
|
+
</Box>
|
|
135
|
+
)}
|
|
136
|
+
<Text dimColor>Press Escape to continue</Text>
|
|
137
|
+
</Box>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
PlanView.displayName = "PlanView";
|
|
@@ -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",
|
|
@@ -80,6 +86,12 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
|
|
|
80
86
|
description: "View and manage workflow runs",
|
|
81
87
|
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
82
88
|
},
|
|
89
|
+
{
|
|
90
|
+
id: "plan",
|
|
91
|
+
name: "plan",
|
|
92
|
+
description: "Enable plan mode or view the current session plan",
|
|
93
|
+
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
94
|
+
},
|
|
83
95
|
{
|
|
84
96
|
id: "clear",
|
|
85
97
|
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,
|
|
@@ -34,10 +35,17 @@ import {
|
|
|
34
35
|
import { logger } from "../utils/logger.js";
|
|
35
36
|
import { displayUsageSummary } from "../utils/usageSummary.js";
|
|
36
37
|
import { expandLongTextPlaceholders } from "../managers/inputHandlers.js";
|
|
38
|
+
import { readFile } from "node:fs/promises";
|
|
37
39
|
|
|
38
40
|
import { BaseAppProps } from "../types.js";
|
|
39
41
|
|
|
40
42
|
// Main Chat Context
|
|
43
|
+
export interface PlanViewData {
|
|
44
|
+
path?: string;
|
|
45
|
+
content?: string;
|
|
46
|
+
message?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
export interface ChatContextType {
|
|
42
50
|
messages: Message[];
|
|
43
51
|
isLoading: boolean;
|
|
@@ -104,9 +112,17 @@ export interface ChatContextType {
|
|
|
104
112
|
agentDefinitions: SubagentConfiguration[];
|
|
105
113
|
// Skill metadata (for /skills overlay)
|
|
106
114
|
skills: SkillMetadata[];
|
|
115
|
+
// Hook configs per scope (for /hooks overlay)
|
|
116
|
+
hooks: Partial<
|
|
117
|
+
Record<"user" | "project" | "plugin", Record<string, HookEventConfig[]>>
|
|
118
|
+
>;
|
|
107
119
|
// Permission functionality
|
|
108
120
|
permissionMode: PermissionMode;
|
|
109
121
|
setPermissionMode: (mode: PermissionMode) => void;
|
|
122
|
+
// /plan overlay state + handler
|
|
123
|
+
planView: PlanViewData | null;
|
|
124
|
+
setPlanView: (view: PlanViewData | null) => void;
|
|
125
|
+
handlePlanCommand: (args?: string) => Promise<void>;
|
|
110
126
|
// Permission confirmation state
|
|
111
127
|
isConfirmationVisible: boolean;
|
|
112
128
|
hasPendingConfirmations: boolean;
|
|
@@ -472,6 +488,12 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
472
488
|
>([]);
|
|
473
489
|
// Skill metadata (for /skills overlay)
|
|
474
490
|
const [skills, setSkills] = useState<SkillMetadata[]>([]);
|
|
491
|
+
// Hook configs per scope (for /hooks overlay)
|
|
492
|
+
const [hooks, setHooks] = useState<
|
|
493
|
+
Partial<
|
|
494
|
+
Record<"user" | "project" | "plugin", Record<string, HookEventConfig[]>>
|
|
495
|
+
>
|
|
496
|
+
>({});
|
|
475
497
|
|
|
476
498
|
// Permission state
|
|
477
499
|
const [permissionMode, setPermissionModeState] = useState<PermissionMode>(
|
|
@@ -479,6 +501,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
479
501
|
(bypassPermissions ? "bypassPermissions" : "default"),
|
|
480
502
|
);
|
|
481
503
|
|
|
504
|
+
// Plan view state for the /plan command overlay
|
|
505
|
+
const [planView, setPlanView] = useState<PlanViewData | null>(null);
|
|
506
|
+
|
|
482
507
|
// Confirmation state with queue-based architecture
|
|
483
508
|
const [isConfirmationVisible, setIsConfirmationVisible] = useState(false);
|
|
484
509
|
const [confirmingTool, setConfirmingTool] = useState<
|
|
@@ -540,7 +565,9 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
540
565
|
// the incremental callbacks in initializeAgent below.
|
|
541
566
|
const refreshMessages = useCallback(() => {
|
|
542
567
|
if (!isExpandedRef.current && agentRef.current) {
|
|
543
|
-
|
|
568
|
+
// Pull the full UI display stream (keeps pre-compaction history);
|
|
569
|
+
// `agent.messages` would only expose the folded API context.
|
|
570
|
+
const msgs = agentRef.current.displayMessages.map(snapshotMessage);
|
|
544
571
|
// Snapshot-safe: the full-list replacement makes the SDK state
|
|
545
572
|
// authoritative. Any update still queued inside the 500ms throttle
|
|
546
573
|
// window was applied to the SDK before this pull, so it is already
|
|
@@ -674,73 +701,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
674
701
|
// delayed by a streaming window).
|
|
675
702
|
updateMessages.flush();
|
|
676
703
|
},
|
|
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
704
|
onLatestTotalTokensChange: (tokens) => {
|
|
745
705
|
setLatestTotalTokens(tokens);
|
|
746
706
|
},
|
|
@@ -865,9 +825,10 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
865
825
|
}
|
|
866
826
|
|
|
867
827
|
// Get initial state — snapshot the SDK messages (never hold live
|
|
868
|
-
// references; see snapshotMessage)
|
|
828
|
+
// references; see snapshotMessage). Uses the full display stream so a
|
|
829
|
+
// restored session shows pre-compaction history too.
|
|
869
830
|
setSessionId(agent.sessionId);
|
|
870
|
-
setMessages(agent.
|
|
831
|
+
setMessages(agent.displayMessages.map(snapshotMessage));
|
|
871
832
|
setIsLoading(agent.isLoading);
|
|
872
833
|
setLatestTotalTokens(extractLatestTotalTokens(agent.messages));
|
|
873
834
|
setIsCommandRunning(agent.isCommandRunning);
|
|
@@ -894,6 +855,19 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
894
855
|
// Get initial skill metadata
|
|
895
856
|
const initialSkills = agent.getSkillMetadata?.() || [];
|
|
896
857
|
setSkills(initialSkills);
|
|
858
|
+
|
|
859
|
+
// Get initial hooks (user/project from settings.json, plugin from
|
|
860
|
+
// the hook manager's plugin-registered set) — snapshot for /hooks.
|
|
861
|
+
const [userHooks, projectHooks, pluginHooks] = await Promise.all([
|
|
862
|
+
agent.getHooksByScope("user"),
|
|
863
|
+
agent.getHooksByScope("project"),
|
|
864
|
+
agent.getHooksByScope("plugin"),
|
|
865
|
+
]);
|
|
866
|
+
setHooks({
|
|
867
|
+
user: userHooks,
|
|
868
|
+
project: projectHooks,
|
|
869
|
+
plugin: pluginHooks,
|
|
870
|
+
});
|
|
897
871
|
} catch (error) {
|
|
898
872
|
console.error("Failed to initialize AI manager:", error);
|
|
899
873
|
}
|
|
@@ -1111,6 +1085,52 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1111
1085
|
});
|
|
1112
1086
|
}, []);
|
|
1113
1087
|
|
|
1088
|
+
// /plan command handler:
|
|
1089
|
+
// - Outside plan mode: switch to plan mode, wait for the plan file path
|
|
1090
|
+
// (generated asynchronously by PlanManager), then either send the given
|
|
1091
|
+
// description as a message (starting a plan query) or show a confirmation.
|
|
1092
|
+
// - Inside plan mode: display the current plan file contents; `/plan open`
|
|
1093
|
+
// is not supported on any end (spec: plan-mode.md "三端均不支持").
|
|
1094
|
+
const handlePlanCommand = useCallback(
|
|
1095
|
+
async (args?: string) => {
|
|
1096
|
+
const agent = agentRef.current;
|
|
1097
|
+
const description = args?.trim() ?? "";
|
|
1098
|
+
const argList = description.split(/\s+/).filter(Boolean);
|
|
1099
|
+
const wantsOpen = argList[0] === "open";
|
|
1100
|
+
|
|
1101
|
+
if (permissionMode !== "plan") {
|
|
1102
|
+
setPermissionMode("plan");
|
|
1103
|
+
// Wait for the plan file path to be generated before triggering a
|
|
1104
|
+
// query so the model always receives the plan file location (spec:
|
|
1105
|
+
// plan-mode.md "路径生成必须先于查询触发").
|
|
1106
|
+
const planPath = await agent?.awaitPlanFilePath();
|
|
1107
|
+
|
|
1108
|
+
if (description && !wantsOpen) {
|
|
1109
|
+
await sendMessage(description);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
setPlanView({ message: "Enabled plan mode", path: planPath });
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Already in plan mode — display the current plan file contents.
|
|
1117
|
+
const planPath =
|
|
1118
|
+
agent?.getPlanFilePath() ?? (await agent?.awaitPlanFilePath());
|
|
1119
|
+
if (!planPath) {
|
|
1120
|
+
setPlanView({ message: "No plan written yet." });
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
const content = await readFile(planPath, "utf8");
|
|
1125
|
+
setPlanView({ path: planPath, content });
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
logger.warn("Failed to read plan file:", error);
|
|
1128
|
+
setPlanView({ message: "No plan written yet." });
|
|
1129
|
+
}
|
|
1130
|
+
},
|
|
1131
|
+
[permissionMode, sendMessage, setPermissionMode],
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1114
1134
|
// MCP management methods - delegate to Agent
|
|
1115
1135
|
const connectMcpServer = useCallback(async (serverName: string) => {
|
|
1116
1136
|
return (await agentRef.current?.connectMcpServer(serverName)) ?? false;
|
|
@@ -1311,8 +1331,12 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1311
1331
|
hasSlashCommand,
|
|
1312
1332
|
agentDefinitions,
|
|
1313
1333
|
skills,
|
|
1334
|
+
hooks,
|
|
1314
1335
|
permissionMode,
|
|
1315
1336
|
setPermissionMode,
|
|
1337
|
+
planView,
|
|
1338
|
+
setPlanView,
|
|
1339
|
+
handlePlanCommand,
|
|
1316
1340
|
isConfirmationVisible,
|
|
1317
1341
|
hasPendingConfirmations: confirmationQueue.length > 0,
|
|
1318
1342
|
confirmingTool,
|