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.
@@ -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",
@@ -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
- const msgs = agentRef.current.messages.map(snapshotMessage);
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.messages.map(snapshotMessage));
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,