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.
- package/dist/bundle/wave.mjs +1382 -1131
- package/package.json +9 -7
- package/src/cli.tsx +14 -22
- package/src/commands/update.ts +62 -22
- package/src/components/AgentsManager.tsx +3 -3
- package/src/components/App.tsx +5 -0
- package/src/components/BackgroundTaskManager.tsx +76 -54
- package/src/components/BtwDisplay.tsx +71 -15
- package/src/components/CompactDisplay.tsx +1 -6
- package/src/components/ConfirmationDetails.tsx +34 -13
- package/src/components/ConfirmationSelector.tsx +31 -36
- package/src/components/DiffDisplay.tsx +1 -1
- package/src/components/HooksManager.tsx +3 -3
- package/src/components/InputBox.tsx +37 -11
- package/src/components/LoginCommand.tsx +16 -35
- package/src/components/Markdown.tsx +7 -3
- package/src/components/MarketplaceAddForm.tsx +15 -19
- package/src/components/MessageBlockItem.tsx +0 -6
- package/src/components/RemoteControlView.tsx +175 -0
- package/src/components/ScrolledContent.tsx +15 -13
- package/src/components/SkillsManager.tsx +3 -3
- package/src/components/TaskList.tsx +3 -3
- package/src/components/ToolDisplay.tsx +3 -18
- package/src/components/WorkflowManager.tsx +150 -136
- package/src/components/WorkflowRunDetail.tsx +193 -0
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +308 -154
- package/src/daemon/commands.ts +31 -22
- package/src/daemon-cli.ts +6 -1
- package/src/hooks/useInputManager.ts +25 -39
- package/src/hooks/useLineScroll.ts +51 -5
- package/src/hooks/useRemoteControl.ts +346 -0
- package/src/index.ts +35 -0
- package/src/managers/inputHandlers.ts +5 -0
- package/src/managers/inputReducer.ts +39 -0
- package/src/print-cli.ts +5 -0
- package/src/reducers/workflowManagerReducer.ts +21 -2
- package/src/remote/frameBudget.ts +45 -0
- package/src/remote/hostIdentity.ts +128 -0
- package/src/remote/messageWindow.ts +46 -0
- package/src/remote/notificationCoalescer.ts +192 -0
- package/src/remote/relayClient.ts +690 -0
- package/src/remote/remoteControlCliHost.ts +420 -0
- package/src/remote/remoteControlCommands.ts +197 -0
- package/src/remote/remoteControlConfig.ts +353 -0
- package/src/remote/remoteControlService.ts +822 -0
- package/src/remote/remoteControlSink.ts +78 -0
- package/src/remote/remoteNotifications.ts +200 -0
- package/src/stdio/agentBridge.ts +874 -106
- package/src/stdio/daemonServer.ts +30 -0
- package/src/stdio/hostSlashCommands.ts +209 -0
- package/src/stdio/jsonRpcConnection.ts +11 -2
- package/src/stdio/protocol.ts +14 -115
- package/src/stdio/stdioServer.ts +33 -2
- package/src/stdio/uiPayload.ts +65 -0
- package/src/stdio-cli.ts +11 -2
- package/src/types.ts +7 -0
- package/src/utils/gitCredentialHelper.ts +106 -0
- package/src/utils/logger.ts +5 -2
- package/src/utils/rewindCheckpoints.ts +2 -4
- package/src/utils/workspaceDiff.ts +620 -0
- package/src/utils/worktree.ts +97 -0
- package/src/utils/bracketedPaste.ts +0 -170
package/src/utils/worktree.ts
CHANGED
|
@@ -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
|
-
}
|