dsh-coding-sidebar 1.0.8 → 1.0.9
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/lib/client-editor.js +223 -175
- package/lib/client-registry.js +763 -344
- package/lib/client-terminal.js +283 -179
- package/lib/client.js +760 -341
- package/lib/index.js +151 -2
- package/lib/types/client/DiffView.d.ts +33 -1
- package/lib/types/client/EditorHost.d.ts +3 -0
- package/lib/types/client/FileTree.d.ts +4 -0
- package/lib/types/client/TerminalWaitBanner.d.ts +6 -0
- package/lib/types/client/TreePanel.d.ts +3 -0
- package/lib/types/client/api.d.ts +26 -0
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/state.d.ts +15 -0
- package/lib/types/fs-operations.d.ts +42 -0
- package/lib/types/git.d.ts +15 -0
- package/package.json +1 -1
- package/src/client/DiffTab.tsx +10 -1
- package/src/client/DiffView.tsx +174 -19
- package/src/client/EditorHost.tsx +8 -1
- package/src/client/FileTree.tsx +147 -4
- package/src/client/Sidebar.tsx +54 -3
- package/src/client/TerminalView.tsx +28 -0
- package/src/client/TerminalWaitBanner.tsx +32 -0
- package/src/client/TreePanel.tsx +6 -1
- package/src/client/api.ts +20 -0
- package/src/client/locales-ar.ts +12 -0
- package/src/client/locales-de.ts +12 -0
- package/src/client/locales-fr.ts +12 -0
- package/src/client/locales-hi.ts +12 -0
- package/src/client/locales-id.ts +12 -0
- package/src/client/locales-it.ts +12 -0
- package/src/client/locales-ja.ts +12 -0
- package/src/client/locales-ko.ts +12 -0
- package/src/client/locales-nl.ts +12 -0
- package/src/client/locales-pl.ts +12 -0
- package/src/client/locales-pt.ts +12 -0
- package/src/client/locales-ru.ts +12 -0
- package/src/client/locales-sv.ts +12 -0
- package/src/client/locales-th.ts +12 -0
- package/src/client/locales-tr.ts +12 -0
- package/src/client/locales-vi.ts +12 -0
- package/src/client/locales-zh-HK.ts +12 -0
- package/src/client/locales-zh-MO.ts +12 -0
- package/src/client/locales-zh-TW.ts +12 -0
- package/src/client/locales.ts +24 -0
- package/src/client/sidebar.module.css +68 -0
- package/src/client/state.ts +42 -3
- package/src/fs-operations.ts +126 -4
- package/src/git.ts +39 -2
- package/src/index.ts +43 -1
package/src/client/state.ts
CHANGED
|
@@ -104,6 +104,14 @@ export interface SidebarState {
|
|
|
104
104
|
splits: SplitNode
|
|
105
105
|
/** Free windows (tabs dragged out onto the conversation area). */
|
|
106
106
|
floats: FloatWindow[]
|
|
107
|
+
/**
|
|
108
|
+
* Live agent-terminal wait state (uuid → the wait the model currently
|
|
109
|
+
* blocks on in `terminal_wait_for`), mirrored from the host's
|
|
110
|
+
* agent-terminals push. Transient by design: sanitizeState never restores
|
|
111
|
+
* it, so a reload starts clean and the next push (sent immediately on WS
|
|
112
|
+
* attach) repopulates it.
|
|
113
|
+
*/
|
|
114
|
+
agentWaits: Record<string, { needle: string; since: number }>
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
export const PANEL_MIN = 280
|
|
@@ -209,6 +217,7 @@ export function makeDefaultState(width = PANEL_DEFAULT, panelOpen = true, seed:
|
|
|
209
217
|
revealed: [],
|
|
210
218
|
splits: leaf,
|
|
211
219
|
floats: [],
|
|
220
|
+
agentWaits: {},
|
|
212
221
|
}
|
|
213
222
|
}
|
|
214
223
|
|
|
@@ -854,6 +863,23 @@ export function agentTabId(uuid: string): string {
|
|
|
854
863
|
return `${AGENT_TAB_PREFIX}${uuid}`
|
|
855
864
|
}
|
|
856
865
|
|
|
866
|
+
/** Shallow equality of two agent-wait maps (same keys, same needle+since). */
|
|
867
|
+
function sameAgentWaits(
|
|
868
|
+
a: SidebarState['agentWaits'] | undefined,
|
|
869
|
+
b: Record<string, { needle: string; since: number }>,
|
|
870
|
+
): boolean {
|
|
871
|
+
if (a === undefined) return Object.keys(b).length === 0
|
|
872
|
+
const aKeys = Object.keys(a)
|
|
873
|
+
if (aKeys.length !== Object.keys(b).length) return false
|
|
874
|
+
for (const key of aKeys) {
|
|
875
|
+
const av = a[key]
|
|
876
|
+
const bv = b[key]
|
|
877
|
+
if (av === undefined || bv === undefined) return false
|
|
878
|
+
if (av.needle !== bv.needle || av.since !== bv.since) return false
|
|
879
|
+
}
|
|
880
|
+
return true
|
|
881
|
+
}
|
|
882
|
+
|
|
857
883
|
/**
|
|
858
884
|
* Reconcile the sidebar's agent-terminal tabs with the host's live list.
|
|
859
885
|
* The host pushes the current list of agent terminals (created by the model
|
|
@@ -868,7 +894,7 @@ export function agentTabId(uuid: string): string {
|
|
|
868
894
|
*/
|
|
869
895
|
export function reconcileAgentTerminals(
|
|
870
896
|
state: SidebarState,
|
|
871
|
-
agentTerminals: ReadonlyArray<{ uuid: string; title: string }>,
|
|
897
|
+
agentTerminals: ReadonlyArray<{ uuid: string; title: string; waiting?: { needle: string; since: number } | null }>,
|
|
872
898
|
): SidebarState {
|
|
873
899
|
const existingTabs = allLeaves(state.splits).flatMap(leaf => leaf.tabs)
|
|
874
900
|
.concat(state.floats.map(float => float.tab))
|
|
@@ -883,7 +909,17 @@ export function reconcileAgentTerminals(
|
|
|
883
909
|
// convergence: no title suffix, no meta write — the tab keeps its uuid
|
|
884
910
|
// so a later reconcile push revives it if the agent reopens the same one).
|
|
885
911
|
const toRemove = existingAgentTabs.filter(tab => !serverUuids.has(agentUuidOf(tab.id)) && tab.pin === undefined)
|
|
886
|
-
|
|
912
|
+
// Mirror the live wait state from the push (authoritative: a vanished
|
|
913
|
+
// waiting field simply drops the entry). A waits-only change must still
|
|
914
|
+
// produce a new state — the tab add/remove no-change check alone would
|
|
915
|
+
// swallow banner updates.
|
|
916
|
+
const serverWaits: Record<string, { needle: string; since: number }> = {}
|
|
917
|
+
for (const terminal of agentTerminals) {
|
|
918
|
+
if (terminal.waiting !== undefined && terminal.waiting !== null) {
|
|
919
|
+
serverWaits[terminal.uuid] = { needle: terminal.waiting.needle, since: terminal.waiting.since }
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (toAdd.length === 0 && toRemove.length === 0 && sameAgentWaits(state.agentWaits, serverWaits)) return state
|
|
887
923
|
// Remove tabs whose uuids vanished from the server list (the agent closed
|
|
888
924
|
// them, or the pty exited and was reaped). Reuse closeTab's leaf cleanup;
|
|
889
925
|
// a FLOATED agent terminal leaves with its window.
|
|
@@ -900,7 +936,7 @@ export function reconcileAgentTerminals(
|
|
|
900
936
|
}
|
|
901
937
|
// Add tabs for new uuids (the agent created a terminal). They land in the
|
|
902
938
|
// active pane via openTabInActivePane; the next reconcile is a no-op for them.
|
|
903
|
-
let next: SidebarState = { ...state, splits, floats }
|
|
939
|
+
let next: SidebarState = { ...state, splits, floats, agentWaits: serverWaits }
|
|
904
940
|
for (const terminal of toAdd) {
|
|
905
941
|
const tab: SidebarTab = {
|
|
906
942
|
id: agentTabId(terminal.uuid),
|
|
@@ -1150,6 +1186,9 @@ export function sanitizeState(parsed: unknown): SidebarState | undefined {
|
|
|
1150
1186
|
revealed: [],
|
|
1151
1187
|
splits: migratedSplits,
|
|
1152
1188
|
floats,
|
|
1189
|
+
// Live wait state is transient: a reload starts clean; the next
|
|
1190
|
+
// agent-terminals push (sent on WS attach) repopulates it.
|
|
1191
|
+
agentWaits: {},
|
|
1153
1192
|
}
|
|
1154
1193
|
}
|
|
1155
1194
|
|
package/src/fs-operations.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workspace-safe file mutations for the sidebar
|
|
2
|
+
* Workspace-safe file mutations for the sidebar: the upload route plus the
|
|
3
|
+
* file tree's rename and delete.
|
|
3
4
|
*
|
|
4
5
|
* Every write is confined to the real session workspace: the upload
|
|
5
6
|
* directory is resolved absolute and its target is checked through existing
|
|
@@ -9,14 +10,21 @@
|
|
|
9
10
|
* to a uniquely named temp sibling
|
|
10
11
|
* and are renamed into place, so a failed, aborted, or oversized upload never
|
|
11
12
|
* leaves a partial file at the target path.
|
|
13
|
+
*
|
|
14
|
+
* The tree's rename/delete (below) are link-aware: existence and containment
|
|
15
|
+
* are verified against the fully resolved target (a symlink pointing outside
|
|
16
|
+
* the workspace is refused), but the operation itself addresses the lexical
|
|
17
|
+
* row path — renaming or deleting a symlink row renames/unlinks the LINK,
|
|
18
|
+
* never its target, matching what the tree row visually names (VS Code
|
|
19
|
+
* semantics). Containment is always enforced (no fence toggle on this fork).
|
|
12
20
|
*/
|
|
13
21
|
import { randomUUID } from 'node:crypto'
|
|
14
22
|
import { once } from 'node:events'
|
|
15
23
|
import { createWriteStream } from 'node:fs'
|
|
16
|
-
import { mkdir, rename, rm, stat } from 'node:fs/promises'
|
|
17
|
-
import { basename, dirname, join } from 'node:path'
|
|
24
|
+
import { access, lstat, mkdir, realpath, rename, rm, stat, unlink } from 'node:fs/promises'
|
|
25
|
+
import { basename, dirname, isAbsolute, join } from 'node:path'
|
|
18
26
|
import type { SidebarHttpRequest } from './context-types.ts'
|
|
19
|
-
import { requireAbsolute } from './fs-tree.ts'
|
|
27
|
+
import { isWithin, requireAbsolute } from './fs-tree.ts'
|
|
20
28
|
import { ensureWorkspacePath, ensureWorkspaceWritePath } from './path-security.ts'
|
|
21
29
|
import { SidebarError } from './wire.ts'
|
|
22
30
|
|
|
@@ -93,3 +101,117 @@ export async function writeWorkspaceUpload(input: WorkspaceUploadInput): Promise
|
|
|
93
101
|
throw error
|
|
94
102
|
}
|
|
95
103
|
}
|
|
104
|
+
|
|
105
|
+
/** Resolve a tree-row path against the session workspace (absolute rows pass through). */
|
|
106
|
+
function resolveRowPath(cwd: string, target: string): string {
|
|
107
|
+
return isAbsolute(target) ? target : join(cwd, target)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Resolve one existing entry for a link-aware mutation: the lexical row path
|
|
111
|
+
* plus its fully resolved real target (containment-checked). Resolution
|
|
112
|
+
* failures become fs-errors, mirroring path-security's semantics. */
|
|
113
|
+
async function resolveEntry(
|
|
114
|
+
cwd: string,
|
|
115
|
+
target: string,
|
|
116
|
+
): Promise<{ absolute: string; real: string; realCwd: string }> {
|
|
117
|
+
const absolute = requireAbsolute(resolveRowPath(cwd, target))
|
|
118
|
+
let real: string
|
|
119
|
+
let realCwd: string
|
|
120
|
+
try {
|
|
121
|
+
;[realCwd, real] = await Promise.all([realpath(cwd), realpath(absolute)])
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new SidebarError('fs-error', `cannot resolve "${target}": ${error instanceof Error ? error.message : String(error)}`, 400)
|
|
124
|
+
}
|
|
125
|
+
if (!isWithin(realCwd, real)) {
|
|
126
|
+
throw new SidebarError('forbidden', `path "${target}" is outside workspace`, 403)
|
|
127
|
+
}
|
|
128
|
+
return { absolute, real, realCwd }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Whether a path exists (ENOENT → false; other failures propagate). */
|
|
132
|
+
async function pathExists(target: string): Promise<boolean> {
|
|
133
|
+
try {
|
|
134
|
+
await access(target)
|
|
135
|
+
return true
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
|
|
138
|
+
throw error
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Inputs of one tree-row rename. */
|
|
143
|
+
export interface WorkspaceRenameInput {
|
|
144
|
+
/** The session workspace root; the renamed entry must stay inside it. */
|
|
145
|
+
cwd: string
|
|
146
|
+
/** Absolute path of the row as the tree displays it (may be a symlink). */
|
|
147
|
+
path: string
|
|
148
|
+
/** The new base name (single segment — rename never moves across directories). */
|
|
149
|
+
name: string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Rename one tree row within its directory: `path` → `<parent>/<name>`.
|
|
154
|
+
* The new name must be a single path segment (this is rename, not move);
|
|
155
|
+
* an existing destination is refused (POSIX rename would clobber it
|
|
156
|
+
* silently); the workspace root itself is never renamable; a symlink row
|
|
157
|
+
* renames the link, not its target. A no-op rename (same name) succeeds
|
|
158
|
+
* without touching the filesystem.
|
|
159
|
+
*
|
|
160
|
+
* @throws SidebarError with a wire code for shape, containment, existence
|
|
161
|
+
* and root failures.
|
|
162
|
+
*/
|
|
163
|
+
export async function renameWorkspaceEntry(input: WorkspaceRenameInput): Promise<{ path: string }> {
|
|
164
|
+
const { cwd, path, name } = input
|
|
165
|
+
if (name === '' || name === '.' || name === '..' || name.includes('/') || name.includes('\\')) {
|
|
166
|
+
throw new SidebarError('bad-request', 'name must be a single path segment', 400)
|
|
167
|
+
}
|
|
168
|
+
const { absolute, real, realCwd } = await resolveEntry(cwd, path)
|
|
169
|
+
if (real === realCwd) {
|
|
170
|
+
throw new SidebarError('fs-error', 'cannot rename the workspace root', 400)
|
|
171
|
+
}
|
|
172
|
+
if (basename(absolute) === name) return { path: absolute }
|
|
173
|
+
const destination = join(dirname(absolute), name)
|
|
174
|
+
const safeDestination = await ensureWorkspaceWritePath(cwd, destination)
|
|
175
|
+
if (await pathExists(safeDestination)) {
|
|
176
|
+
throw new SidebarError('fs-error', `"${name}" already exists`, 409)
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
await rename(absolute, safeDestination)
|
|
180
|
+
} catch (error) {
|
|
181
|
+
throw new SidebarError('fs-error', `cannot rename "${path}" to "${name}": ${error instanceof Error ? error.message : String(error)}`, 400)
|
|
182
|
+
}
|
|
183
|
+
return { path: safeDestination }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Inputs of one tree-row delete. */
|
|
187
|
+
export interface WorkspaceRemoveInput {
|
|
188
|
+
/** The session workspace root; the removed entry must stay inside it. */
|
|
189
|
+
cwd: string
|
|
190
|
+
/** Absolute path of the row as the tree displays it (may be a symlink). */
|
|
191
|
+
path: string
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Delete one tree row permanently (there is no trash on the host): files are
|
|
196
|
+
* unlinked, directories removed recursively, a symlink row unlinks the LINK
|
|
197
|
+
* only (lstat decides, so a link to a directory does not recurse into its
|
|
198
|
+
* target). The workspace root itself is never removable.
|
|
199
|
+
*
|
|
200
|
+
* @throws SidebarError with a wire code for containment, existence and
|
|
201
|
+
* root failures.
|
|
202
|
+
*/
|
|
203
|
+
export async function removeWorkspaceEntry(input: WorkspaceRemoveInput): Promise<{ path: string }> {
|
|
204
|
+
const { cwd, path } = input
|
|
205
|
+
const { absolute, real, realCwd } = await resolveEntry(cwd, path)
|
|
206
|
+
if (real === realCwd) {
|
|
207
|
+
throw new SidebarError('fs-error', 'cannot remove the workspace root', 400)
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const info = await lstat(absolute)
|
|
211
|
+
if (info.isDirectory()) await rm(absolute, { recursive: true })
|
|
212
|
+
else await unlink(absolute)
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw new SidebarError('fs-error', `cannot remove "${path}": ${error instanceof Error ? error.message : String(error)}`, 400)
|
|
215
|
+
}
|
|
216
|
+
return { path: absolute }
|
|
217
|
+
}
|
package/src/git.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Commits use the user's git global identity untouched (never sets
|
|
10
10
|
* user.name/user.email).
|
|
11
11
|
*/
|
|
12
|
-
import { readdir } from 'node:fs/promises'
|
|
13
|
-
import { join } from 'node:path'
|
|
12
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
13
|
+
import { isAbsolute, join } from 'node:path'
|
|
14
14
|
import { spawn } from 'node:child_process'
|
|
15
15
|
import { resolve } from 'node:path'
|
|
16
16
|
|
|
@@ -439,6 +439,43 @@ export async function show(cwd: string, rev: string, path: string, selected?: st
|
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Both sides' full file contents for a diff-fold expansion. `path` is
|
|
444
|
+
* repo-relative. The sides resolve per diff kind: a commit reads
|
|
445
|
+
* `<hash>^` vs `<hash>`; a staged change reads HEAD vs the index (`:`);
|
|
446
|
+
* an unstaged change reads HEAD vs the working tree file on disk (a side
|
|
447
|
+
* that does not exist — untracked, deleted, binary-refused — comes back
|
|
448
|
+
* null and the client degrades the fold to a static marker).
|
|
449
|
+
*/
|
|
450
|
+
export async function foldContents(
|
|
451
|
+
cwd: string,
|
|
452
|
+
path: string,
|
|
453
|
+
opts: { staged?: boolean; hash?: string } = {},
|
|
454
|
+
selected?: string,
|
|
455
|
+
): Promise<{ old: string | null; new: string | null }> {
|
|
456
|
+
const root = await repoRoot(cwd, selected)
|
|
457
|
+
if (opts.hash !== undefined) {
|
|
458
|
+
const [old, neu] = await Promise.all([
|
|
459
|
+
show(root, `${opts.hash}^`, path, selected),
|
|
460
|
+
show(root, opts.hash, path, selected),
|
|
461
|
+
])
|
|
462
|
+
return { old, new: neu }
|
|
463
|
+
}
|
|
464
|
+
if (opts.staged === true) {
|
|
465
|
+
// `git show :<path>` reads the index (stage 0) — the staged side.
|
|
466
|
+
const [old, neu] = await Promise.all([
|
|
467
|
+
show(root, 'HEAD', path, selected),
|
|
468
|
+
show(root, ':', path, selected),
|
|
469
|
+
])
|
|
470
|
+
return { old, new: neu }
|
|
471
|
+
}
|
|
472
|
+
const [old, neu] = await Promise.all([
|
|
473
|
+
show(root, 'HEAD', path, selected),
|
|
474
|
+
readFile(isAbsolute(path) ? path : join(root, path), 'utf8').catch(() => null),
|
|
475
|
+
])
|
|
476
|
+
return { old, new: neu }
|
|
477
|
+
}
|
|
478
|
+
|
|
442
479
|
/** Full patch text of one commit (`git show` with the commit header suppressed).
|
|
443
480
|
* Merge commits show their diff against the first parent (`-m --first-parent`
|
|
444
481
|
* is a no-op for regular commits), so a history click always has content. */
|
package/src/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
type SidebarPrefs,
|
|
31
31
|
} from './config.ts'
|
|
32
32
|
import { parentOf, requireAbsolute, listDirectory, rootLabel } from './fs-tree.ts'
|
|
33
|
-
import { writeWorkspaceUpload } from './fs-operations.ts'
|
|
33
|
+
import { removeWorkspaceEntry, renameWorkspaceEntry, writeWorkspaceUpload } from './fs-operations.ts'
|
|
34
34
|
import { ensureWorkspacePath, ensureWorkspaceWritePath } from './path-security.ts'
|
|
35
35
|
import { searchFiles } from './fs-search.ts'
|
|
36
36
|
import { extractFrameAncestors } from './browser-probe.ts'
|
|
@@ -351,6 +351,26 @@ function buildApi(
|
|
|
351
351
|
}
|
|
352
352
|
return { ok: true }
|
|
353
353
|
},
|
|
354
|
+
// The tree row's rename: single-segment name, destination-existence and
|
|
355
|
+
// workspace-root refusals, link-aware (renames the row, not its target).
|
|
356
|
+
// fs-operations.ts owns the containment and shape rules.
|
|
357
|
+
'fs.rename': async (payload) => {
|
|
358
|
+
const { cwd } = await cwdOf(payload)
|
|
359
|
+
return renameWorkspaceEntry({
|
|
360
|
+
cwd,
|
|
361
|
+
path: requireString(payload, 'path'),
|
|
362
|
+
name: requireString(payload, 'name'),
|
|
363
|
+
})
|
|
364
|
+
},
|
|
365
|
+
// The tree row's delete (permanent — the host has no trash): recursive
|
|
366
|
+
// for directories, unlinks a symlink row without touching its target.
|
|
367
|
+
'fs.remove': async (payload) => {
|
|
368
|
+
const { cwd } = await cwdOf(payload)
|
|
369
|
+
return removeWorkspaceEntry({
|
|
370
|
+
cwd,
|
|
371
|
+
path: requireString(payload, 'path'),
|
|
372
|
+
})
|
|
373
|
+
},
|
|
354
374
|
'git.worktrees': async (payload) => {
|
|
355
375
|
const { cwd } = await gitCwdOf(payload)
|
|
356
376
|
const selected = selectedRepoOf(payload)
|
|
@@ -438,6 +458,19 @@ function buildApi(
|
|
|
438
458
|
const rev = requireString(payload, 'rev')
|
|
439
459
|
return { content: await git.show(cwd, rev, path, repoRoot) }
|
|
440
460
|
},
|
|
461
|
+
// Diff-fold expansion data: both sides' full file contents so the client
|
|
462
|
+
// can materialize the hidden context rows a -U3 hunk gap omitted. The
|
|
463
|
+
// sides resolve per diff kind (commit hash / staged / unstaged) inside
|
|
464
|
+
// git.foldContents; a missing side is null and the client degrades.
|
|
465
|
+
'git.fold-contents': async (payload) => {
|
|
466
|
+
const { cwd } = await gitCwdOf(payload)
|
|
467
|
+
const repoRoot = selectedRepoOf(payload)
|
|
468
|
+
const record = payload as { path?: unknown; staged?: unknown; hash?: unknown }
|
|
469
|
+
const path = await resolveGitPath(cwd, requireString(record, 'path'), repoRoot)
|
|
470
|
+
const staged = record.staged === true
|
|
471
|
+
const hash = typeof record.hash === 'string' ? record.hash : undefined
|
|
472
|
+
return await git.foldContents(cwd, path, { staged, hash }, repoRoot)
|
|
473
|
+
},
|
|
441
474
|
// Release a terminal immediately. The WebSocket close frame already does
|
|
442
475
|
// this while the socket is open; this route covers the tab-close that
|
|
443
476
|
// happens while the socket is down (reconnect loop), so a closed tab can
|
|
@@ -459,6 +492,15 @@ function buildApi(
|
|
|
459
492
|
agentPtyRegistry?.close(uuid)
|
|
460
493
|
return { ok: true }
|
|
461
494
|
},
|
|
495
|
+
// The sidebar wait banner's skip button: abort every active
|
|
496
|
+
// terminal_wait_for on one agent terminal. Idempotent — 0 when nothing
|
|
497
|
+
// is waiting (a stale banner racing a wait that already resolved).
|
|
498
|
+
// Degraded mode (node-pty unavailable) has no registry and no waits: an
|
|
499
|
+
// honest ok.
|
|
500
|
+
'agent-pty.skip-wait': (payload) => {
|
|
501
|
+
const uuid = requireString(payload, 'uuid')
|
|
502
|
+
return { ok: true, skipped: agentPtyRegistry?.skipWait(uuid) ?? 0 }
|
|
503
|
+
},
|
|
462
504
|
// Terminal dependency status (issue #140): after a WS close 1011 with
|
|
463
505
|
// reason `pty-deps-missing` the client fetches the full repair details
|
|
464
506
|
// here — the close reason itself is capped at 123 bytes, too small for
|