wave-code 1.3.0 → 1.5.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 (63) hide show
  1. package/dist/bundle/wave.mjs +1382 -1131
  2. package/package.json +9 -7
  3. package/src/cli.tsx +14 -22
  4. package/src/commands/update.ts +62 -22
  5. package/src/components/AgentsManager.tsx +3 -3
  6. package/src/components/App.tsx +5 -0
  7. package/src/components/BackgroundTaskManager.tsx +76 -54
  8. package/src/components/BtwDisplay.tsx +71 -15
  9. package/src/components/CompactDisplay.tsx +1 -6
  10. package/src/components/ConfirmationDetails.tsx +34 -13
  11. package/src/components/ConfirmationSelector.tsx +31 -36
  12. package/src/components/DiffDisplay.tsx +1 -1
  13. package/src/components/HooksManager.tsx +3 -3
  14. package/src/components/InputBox.tsx +37 -11
  15. package/src/components/LoginCommand.tsx +16 -35
  16. package/src/components/Markdown.tsx +7 -3
  17. package/src/components/MarketplaceAddForm.tsx +15 -19
  18. package/src/components/MessageBlockItem.tsx +0 -6
  19. package/src/components/RemoteControlView.tsx +175 -0
  20. package/src/components/ScrolledContent.tsx +15 -13
  21. package/src/components/SkillsManager.tsx +3 -3
  22. package/src/components/TaskList.tsx +3 -3
  23. package/src/components/ToolDisplay.tsx +3 -18
  24. package/src/components/WorkflowManager.tsx +150 -136
  25. package/src/components/WorkflowRunDetail.tsx +193 -0
  26. package/src/constants/commands.ts +6 -0
  27. package/src/contexts/useChat.tsx +308 -154
  28. package/src/daemon/commands.ts +31 -22
  29. package/src/daemon-cli.ts +6 -1
  30. package/src/hooks/useInputManager.ts +25 -39
  31. package/src/hooks/useLineScroll.ts +51 -5
  32. package/src/hooks/useRemoteControl.ts +346 -0
  33. package/src/index.ts +35 -0
  34. package/src/managers/inputHandlers.ts +5 -0
  35. package/src/managers/inputReducer.ts +39 -0
  36. package/src/print-cli.ts +5 -0
  37. package/src/reducers/workflowManagerReducer.ts +21 -2
  38. package/src/remote/frameBudget.ts +45 -0
  39. package/src/remote/hostIdentity.ts +128 -0
  40. package/src/remote/messageWindow.ts +46 -0
  41. package/src/remote/notificationCoalescer.ts +192 -0
  42. package/src/remote/relayClient.ts +690 -0
  43. package/src/remote/remoteControlCliHost.ts +420 -0
  44. package/src/remote/remoteControlCommands.ts +197 -0
  45. package/src/remote/remoteControlConfig.ts +353 -0
  46. package/src/remote/remoteControlService.ts +822 -0
  47. package/src/remote/remoteControlSink.ts +78 -0
  48. package/src/remote/remoteNotifications.ts +200 -0
  49. package/src/stdio/agentBridge.ts +874 -106
  50. package/src/stdio/daemonServer.ts +30 -0
  51. package/src/stdio/hostSlashCommands.ts +209 -0
  52. package/src/stdio/jsonRpcConnection.ts +11 -2
  53. package/src/stdio/protocol.ts +14 -115
  54. package/src/stdio/stdioServer.ts +33 -2
  55. package/src/stdio/uiPayload.ts +65 -0
  56. package/src/stdio-cli.ts +11 -2
  57. package/src/types.ts +7 -0
  58. package/src/utils/gitCredentialHelper.ts +106 -0
  59. package/src/utils/logger.ts +5 -2
  60. package/src/utils/rewindCheckpoints.ts +2 -4
  61. package/src/utils/workspaceDiff.ts +620 -0
  62. package/src/utils/worktree.ts +97 -0
  63. package/src/utils/bracketedPaste.ts +0 -170
@@ -407,6 +407,103 @@ export interface WorktreeChanges {
407
407
  commits: number;
408
408
  }
409
409
 
410
+ /**
411
+ * The linked worktree a directory sits in, derived from the filesystem only.
412
+ * Same field semantics as `createWorktree`'s result, so a caller can hand it
413
+ * straight to `removeWorktree`.
414
+ */
415
+ export interface DerivedWorktree {
416
+ path: string;
417
+ branch: string;
418
+ repoRoot: string;
419
+ hookBased: boolean;
420
+ }
421
+
422
+ /**
423
+ * Work out whether `sessionDir` **is** a linked git worktree — and which one —
424
+ * without running git.
425
+ *
426
+ * Two callers need this and both need it cheap: `listDaemonSessions` embeds the
427
+ * answer in every session row (the remote page decides from it whether deleting
428
+ * a session also deletes a worktree), and `destroy --removeWorktree` needs the
429
+ * path/branch/repoRoot that `removeWorktree` takes. Shelling out per row is not
430
+ * an option — `listDaemonSessions` runs on every reconnect, snapshot and page
431
+ * open, and a synchronous git call in this process freezes every session it
432
+ * serves (see the note at the top of this file). So the whole derivation is
433
+ * plain fs reads:
434
+ *
435
+ * - `.git` a **directory** in `sessionDir` ⇒ it is the main working tree, not a
436
+ * worktree ⇒ `undefined` (removing it would be removing the repository).
437
+ * - `.git` a **file** ⇒ a linked worktree; the file holds `gitdir: <path>`,
438
+ * which is where that worktree's own HEAD lives.
439
+ * - branch = `<gitdir>/HEAD` (`ref: refs/heads/<name>`); a detached HEAD has no
440
+ * branch, which returns `""` — the same thing `git branch --show-current`
441
+ * says, and `removeWorktree` skips a branch it cannot name.
442
+ * - repoRoot = the main repository root, i.e. the directory *above* the common
443
+ * git dir that `<gitdir>/commondir` points at (`<repoRoot>/.git`).
444
+ *
445
+ * `sessionDir` must be the worktree root itself (the directory a session was
446
+ * created in) — a subdirectory of a worktree has no `.git` of its own, and this
447
+ * returns `undefined` for it rather than guessing. Every failure path returns
448
+ * `undefined`: "not a worktree / cannot tell" is a legitimate answer and must
449
+ * never throw into a caller that is only decorating a list.
450
+ */
451
+ export function deriveWorktreeInfo(
452
+ sessionDir: string,
453
+ ): DerivedWorktree | undefined {
454
+ try {
455
+ if (!sessionDir) return undefined;
456
+ const dir = path.resolve(sessionDir);
457
+ const gitEntry = path.join(dir, ".git");
458
+ if (!fs.statSync(gitEntry).isFile()) return undefined;
459
+
460
+ const prefix = "gitdir: ";
461
+ const content = fs.readFileSync(gitEntry, "utf8").trim();
462
+ if (!content.startsWith(prefix)) return undefined;
463
+ const gitDir = path.resolve(dir, content.substring(prefix.length).trim());
464
+
465
+ let branch = "";
466
+ try {
467
+ const head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
468
+ const refPrefix = "ref: refs/heads/";
469
+ if (head.startsWith(refPrefix)) {
470
+ branch = head.substring(refPrefix.length);
471
+ }
472
+ } catch {
473
+ // Detached/unreadable HEAD: no branch to delete, the worktree is still
474
+ // identifiable.
475
+ }
476
+
477
+ let repoRoot = "";
478
+ try {
479
+ const common = fs
480
+ .readFileSync(path.join(gitDir, "commondir"), "utf8")
481
+ .trim();
482
+ const commonDir = path.resolve(gitDir, common);
483
+ repoRoot =
484
+ path.basename(commonDir) === ".git"
485
+ ? path.dirname(commonDir)
486
+ : commonDir;
487
+ } catch {
488
+ return undefined;
489
+ }
490
+ if (!repoRoot) return undefined;
491
+
492
+ // Hook-based worktrees are removed by the WorktreeRemove hook, never by
493
+ // `git worktree remove` — mirror createWorktree's own hookBased decision
494
+ // (same test `wave daemon destroy --remove-worktree` uses).
495
+ const config = loadMergedWaveConfig(repoRoot);
496
+ return {
497
+ path: dir,
498
+ branch,
499
+ repoRoot,
500
+ hookBased: hasWorktreeCreateHook(config?.hooks),
501
+ };
502
+ } catch {
503
+ return undefined;
504
+ }
505
+ }
506
+
410
507
  /**
411
508
  * Count what deleting a worktree would throw away, so the caller can warn
412
509
  * before it happens. Both counts are best-effort: an unknown base branch (never
@@ -1,170 +0,0 @@
1
- /**
2
- * Bracketed paste (DECSET 2004) detector.
3
- *
4
- * When bracketed paste is enabled, terminals wrap pasted text in
5
- * `\x1b[200~` (start) and `\x1b[201~` (end) markers, which lets the input
6
- * pipeline distinguish "text was pasted" from "user typed keys" — most
7
- * importantly, a pasted trailing `\r` must NOT be treated as an Enter key.
8
- *
9
- * ink delivers each stdin chunk through useInput after stripping ONE leading
10
- * ESC from the parsed sequence (see use-input.js), so markers may arrive in
11
- * either the raw form (`\x1b[200~`) or the stripped form (`[200~`), and may
12
- * be split across chunks. The detector normalizes both forms and buffers
13
- * partial markers until they complete (deferral is safe: a deferred partial
14
- * is flushed unchanged if the next chunk does not complete a marker).
15
- */
16
- export type PasteProcessResult =
17
- | { kind: "input"; input: string }
18
- | { kind: "paste"; text: string; leadingInput?: string }
19
- | { kind: "consume" };
20
-
21
- export interface BracketedPasteDetector {
22
- /**
23
- * Feed one input chunk (as delivered by ink's useInput callback).
24
- * - `input`: regular keystrokes, pass through to normal handling.
25
- * - `paste`: completed bracketed paste; insert `text` without submitting.
26
- * `leadingInput` (rare) is content that preceded the start marker in the
27
- * same chunk and should be handled as regular input first.
28
- * - `consume`: content of an in-flight paste (or an empty paste); do
29
- * nothing with this chunk.
30
- */
31
- process(chunk: string): PasteProcessResult;
32
- reset(): void;
33
- }
34
-
35
- const ESC = "\u001b";
36
- const START_STRIPPED = "[200~";
37
- const START_RAW = `${ESC}[200~`;
38
- const END_STRIPPED = "[201~";
39
- const END_RAW = `${ESC}[201~`;
40
-
41
- const START_FORMS = [START_RAW, START_STRIPPED];
42
- const END_FORMS = [END_RAW, END_STRIPPED];
43
-
44
- /**
45
- * Suffixes that may be the beginning of a marker split across chunks.
46
- * Longest-first so the longest matching suffix is deferred (e.g. `[200`
47
- * rather than `[20` for `...x[200`). Deferral is flush-equivalent: if the
48
- * next chunk does not complete a marker, the deferred suffix is delivered
49
- * as regular input unchanged.
50
- */
51
- const PARTIAL_SUFFIXES = [
52
- `${ESC}[201`,
53
- `${ESC}[200`,
54
- `${ESC}[20`,
55
- `${ESC}[2`,
56
- "[201",
57
- "[200",
58
- "[20",
59
- "[2",
60
- ].sort((a, b) => b.length - a.length);
61
-
62
- export function createBracketedPasteDetector(): BracketedPasteDetector {
63
- let inPaste = false;
64
- let buffer = ""; // paste content collected since the start marker
65
- let pending = ""; // deferred partial marker suffix from a previous chunk
66
-
67
- const findFirst = (
68
- text: string,
69
- forms: string[],
70
- ): { index: number; length: number } | null => {
71
- let best: { index: number; length: number } | null = null;
72
- for (const form of forms) {
73
- const index = text.indexOf(form);
74
- if (index !== -1 && (best === null || index < best.index)) {
75
- best = { index, length: form.length };
76
- }
77
- }
78
- return best;
79
- };
80
-
81
- const endsWithPartial = (text: string): string | null => {
82
- for (const prefix of PARTIAL_SUFFIXES) {
83
- if (text.endsWith(prefix)) {
84
- return prefix;
85
- }
86
- }
87
- return null;
88
- };
89
-
90
- const process = (chunk: string): PasteProcessResult => {
91
- let remaining = pending + chunk;
92
- pending = "";
93
- let leadingInput = "";
94
- let pasteText: string | null = null;
95
-
96
- while (remaining.length > 0) {
97
- if (!inPaste) {
98
- const start = findFirst(remaining, START_FORMS);
99
- if (start) {
100
- leadingInput += remaining.slice(0, start.index);
101
- remaining = remaining.slice(start.index + start.length);
102
- inPaste = true;
103
- continue;
104
- }
105
- // Orphan end marker (start was missed, e.g. consumed by another
106
- // input handler): treat the preceding text as paste so a trailing
107
- // `\r` cannot trigger the coalesced-Enter submit heuristic.
108
- const end = findFirst(remaining, END_FORMS);
109
- if (end) {
110
- pasteText =
111
- (pasteText ?? "") + leadingInput + remaining.slice(0, end.index);
112
- leadingInput = "";
113
- remaining = remaining.slice(end.index + end.length);
114
- continue;
115
- }
116
- const partial = endsWithPartial(remaining);
117
- if (partial) {
118
- pending = remaining.slice(remaining.length - partial.length);
119
- remaining = remaining.slice(0, remaining.length - partial.length);
120
- }
121
- if (remaining.length > 0) {
122
- leadingInput += remaining;
123
- remaining = "";
124
- }
125
- break;
126
- }
127
-
128
- const end = findFirst(remaining, END_FORMS);
129
- if (end) {
130
- pasteText = (pasteText ?? "") + buffer + remaining.slice(0, end.index);
131
- buffer = "";
132
- inPaste = false;
133
- remaining = remaining.slice(end.index + end.length);
134
- continue;
135
- }
136
- const partial = endsWithPartial(remaining);
137
- if (partial) {
138
- pending = remaining.slice(remaining.length - partial.length);
139
- remaining = remaining.slice(0, remaining.length - partial.length);
140
- }
141
- buffer += remaining;
142
- remaining = "";
143
- break;
144
- }
145
-
146
- if (inPaste) {
147
- // Paste still in flight: hold the content, deliver nothing yet.
148
- return { kind: "consume" };
149
- }
150
- if (pasteText !== null) {
151
- const result: { kind: "paste"; text: string; leadingInput?: string } = {
152
- kind: "paste",
153
- text: pasteText,
154
- };
155
- if (leadingInput !== "") {
156
- result.leadingInput = leadingInput;
157
- }
158
- return result;
159
- }
160
- return { kind: "input", input: leadingInput };
161
- };
162
-
163
- const reset = (): void => {
164
- inPaste = false;
165
- buffer = "";
166
- pending = "";
167
- };
168
-
169
- return { process, reset };
170
- }