dsh-coding-sidebar 1.0.8 → 1.0.10

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 (58) hide show
  1. package/README.md +2 -2
  2. package/lib/client-editor.js +257 -178
  3. package/lib/client-registry.js +959 -345
  4. package/lib/client-terminal.js +307 -172
  5. package/lib/client.js +952 -338
  6. package/lib/index.js +214 -2
  7. package/lib/types/changes-ops.d.ts +31 -0
  8. package/lib/types/client/DiffView.d.ts +33 -1
  9. package/lib/types/client/EditorHost.d.ts +3 -0
  10. package/lib/types/client/FileTree.d.ts +4 -0
  11. package/lib/types/client/SessionLens.d.ts +4 -0
  12. package/lib/types/client/TerminalWaitBanner.d.ts +6 -0
  13. package/lib/types/client/TreePanel.d.ts +3 -0
  14. package/lib/types/client/api.d.ts +36 -0
  15. package/lib/types/client/locales.d.ts +20 -0
  16. package/lib/types/client/redact.d.ts +14 -0
  17. package/lib/types/client/state.d.ts +15 -0
  18. package/lib/types/fs-operations.d.ts +42 -0
  19. package/lib/types/git.d.ts +15 -0
  20. package/package.json +1 -1
  21. package/src/changes-ops.ts +78 -0
  22. package/src/client/DiffTab.tsx +10 -1
  23. package/src/client/DiffView.tsx +174 -19
  24. package/src/client/EditorHost.tsx +8 -1
  25. package/src/client/FileTree.tsx +147 -4
  26. package/src/client/GitView.tsx +37 -0
  27. package/src/client/SessionLens.tsx +95 -0
  28. package/src/client/Sidebar.tsx +54 -3
  29. package/src/client/TerminalView.tsx +28 -0
  30. package/src/client/TerminalWaitBanner.tsx +32 -0
  31. package/src/client/TreePanel.tsx +6 -1
  32. package/src/client/api.ts +24 -0
  33. package/src/client/locales-ar.ts +20 -0
  34. package/src/client/locales-de.ts +20 -0
  35. package/src/client/locales-fr.ts +20 -0
  36. package/src/client/locales-hi.ts +20 -0
  37. package/src/client/locales-id.ts +20 -0
  38. package/src/client/locales-it.ts +20 -0
  39. package/src/client/locales-ja.ts +20 -0
  40. package/src/client/locales-ko.ts +20 -0
  41. package/src/client/locales-nl.ts +20 -0
  42. package/src/client/locales-pl.ts +20 -0
  43. package/src/client/locales-pt.ts +20 -0
  44. package/src/client/locales-ru.ts +20 -0
  45. package/src/client/locales-sv.ts +20 -0
  46. package/src/client/locales-th.ts +20 -0
  47. package/src/client/locales-tr.ts +20 -0
  48. package/src/client/locales-vi.ts +20 -0
  49. package/src/client/locales-zh-HK.ts +20 -0
  50. package/src/client/locales-zh-MO.ts +20 -0
  51. package/src/client/locales-zh-TW.ts +20 -0
  52. package/src/client/locales.ts +40 -0
  53. package/src/client/redact.ts +39 -0
  54. package/src/client/sidebar.module.css +183 -0
  55. package/src/client/state.ts +42 -3
  56. package/src/fs-operations.ts +126 -4
  57. package/src/git.ts +39 -2
  58. package/src/index.ts +56 -1
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The session lens: file operations the model performed in this session,
3
+ * parsed from the session's own event log (`changes.ops`). Clicking a row
4
+ * expands a best-effort text preview of the file (read through `fs.read`,
5
+ * passed through the secret-redaction layer). Kept a leaf component — the
6
+ * GitView hosts it as the "session changes" lens of the unified tab.
7
+ */
8
+ import { useCallback, useEffect, useState } from 'react'
9
+ import { IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
10
+ import { api, type SessionScope } from './api.ts'
11
+ import { redactSecrets } from './redact.ts'
12
+ import { t } from './locales.ts'
13
+ import css from './sidebar.module.css'
14
+
15
+ /** One deduplicated file operation row (the host's `changes.ops` payload). */
16
+ interface SessionFileOp {
17
+ path: string
18
+ tool: string
19
+ time: number
20
+ count: number
21
+ }
22
+
23
+ /** Preview cap: a long file shows its head only (the sidebar is not an editor). */
24
+ const PREVIEW_CHARS = 20_000
25
+
26
+ export function SessionLens(props: { scope: SessionScope }) {
27
+ const { scope } = props
28
+ const [ops, setOps] = useState<SessionFileOp[] | null>(null)
29
+ const [error, setError] = useState<string | null>(null)
30
+ const [openPath, setOpenPath] = useState<string | null>(null)
31
+ const [preview, setPreview] = useState<string | null>(null)
32
+ const [previewLoading, setPreviewLoading] = useState(false)
33
+
34
+ const load = useCallback(async (): Promise<void> => {
35
+ setError(null)
36
+ try {
37
+ const result = await api.changesOps(scope)
38
+ setOps(result.ops)
39
+ } catch (reason) {
40
+ setError(reason instanceof Error ? reason.message : String(reason))
41
+ }
42
+ }, [scope])
43
+
44
+ useEffect(() => { void load() }, [load])
45
+
46
+ /** Toggle one row's preview: fetch + redact on first open, cached after. */
47
+ const togglePreview = (path: string): void => {
48
+ if (openPath === path) {
49
+ setOpenPath(null)
50
+ return
51
+ }
52
+ setOpenPath(path)
53
+ setPreview(null)
54
+ setPreviewLoading(true)
55
+ api.fsRead(scope, path).then((result) => {
56
+ const text = result.kind === 'text' ? redactSecrets(result.content) : t('changesBinary')
57
+ setPreview(text.slice(0, PREVIEW_CHARS))
58
+ }).catch((reason: unknown) => {
59
+ setPreview(t('changesPreviewError', { message: reason instanceof Error ? reason.message : String(reason) }))
60
+ }).finally(() => { setPreviewLoading(false) })
61
+ }
62
+
63
+ return (
64
+ <div className={css.sessionLens}>
65
+ <div className={css.sessionLensBar}>
66
+ <span className={css.sessionLensCount}>{ops === null ? t('loading') : t('changesCount', { count: ops.length })}</span>
67
+ <button type="button" className={css.iconButton} aria-label={t('refresh')} title={t('refresh')} onClick={() => { void load() }}>
68
+ <IconRefreshOutline16 size={14} />
69
+ </button>
70
+ </div>
71
+ {error !== null && <div className={css.sessionLensEmpty}>{error}</div>}
72
+ {error === null && ops !== null && ops.length === 0 && (
73
+ <div className={css.sessionLensEmpty}>{t('changesEmpty')}</div>
74
+ )}
75
+ {ops !== null && ops.map(op => (
76
+ <div key={op.path} className={css.sessionLensItem}>
77
+ <button
78
+ type="button"
79
+ className={css.sessionLensRow}
80
+ aria-expanded={openPath === op.path}
81
+ onClick={() => { togglePreview(op.path) }}
82
+ >
83
+ <span className={css.sessionLensPath} title={op.path}>{op.path}</span>
84
+ <span className={css.sessionLensMeta}>{op.tool}{op.count > 1 ? ` ×${op.count}` : ''}</span>
85
+ </button>
86
+ {openPath === op.path && (
87
+ <div className={css.sessionLensPreview}>
88
+ {previewLoading ? t('loading') : preview ?? ''}
89
+ </div>
90
+ )}
91
+ </div>
92
+ ))}
93
+ </div>
94
+ )
95
+ }
@@ -28,11 +28,13 @@ import type { Context, SidebarSessionList } from '../context-types.ts'
28
28
  import { appendToDraft, insertFileReference } from './conversation-draft.ts'
29
29
  import {
30
30
  PANEL_MIN, activateTab, agentUuidOf, closeFloatByTab, closeTab, dockFloat, firstLeaf, floatTab,
31
- isAgentTabId, leafWithTab,
31
+ isAgentTabId, leafWithTab, allLeaves,
32
32
  moveFloat, moveTab, moveTabToEdge, openDiffTab, raiseFloat, reconcileAgentTerminals,
33
33
  resizeFloat, resizeSplitIn, setTabPin, setWidth, toggleExpanded, togglePanel,
34
34
  type DropZone, type SidebarState, type SidebarStore, type SidebarTab,
35
35
  } from './state.ts'
36
+ import { baseName } from './FileTree.tsx'
37
+ import { isWithinWorkspace } from './paths.ts'
36
38
  import { collectPinnedTabs, createPinnedVirtualTab, getPinnedHomeScope, injectPinnedIntoTree, isPinnedVirtualId, isPinnedVirtualTab, parsePinnedVirtualId, type PinnedTabEntry } from './pinned.ts'
37
39
  import { IconPinOutline16 } from './icons.tsx'
38
40
  import { IconPanelRightOutline16 } from './icons.tsx'
@@ -126,11 +128,14 @@ interface TabContentProps extends TabContentMemoKey {
126
128
  onSubagentJump: (childSessionId: string) => void
127
129
  /** Open a diff tab from the git panel (placement handled by the store). */
128
130
  onOpenDiff: (tab: SidebarTab) => void
131
+ /** Tree-row mutations (threaded to the file tree; see Sidebar's handlers). */
132
+ onPathRenamed?: (oldPath: string, newPath: string) => void
133
+ onPathRemoved?: (path: string) => void
129
134
  }
130
135
 
131
136
  /** Render the content of one tab (dispatched by type). */
132
137
  const TabContent = memo(function TabContent(props: TabContentProps) {
133
- const { tab, effectiveTabId, sessionId, cwd, expanded, revealed, onToggleDir, onReferenceFile, ctx, store, visible, onSubagentJump, onOpenDiff } = props
138
+ const { tab, effectiveTabId, sessionId, cwd, expanded, revealed, onToggleDir, onReferenceFile, ctx, store, visible, onSubagentJump, onOpenDiff, onPathRenamed, onPathRemoved } = props
134
139
  const scope = { sessionId, cwd }
135
140
  const descriptor = ctx.get('betterSidebar')?.getTab(tab.type)
136
141
  if (descriptor === undefined) {
@@ -394,7 +399,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
394
399
  socket.onmessage = (event) => {
395
400
  if (typeof event.data !== 'string') return
396
401
  try {
397
- const list = JSON.parse(event.data) as Array<{ uuid: string; title: string; command: string; exited: boolean }>
402
+ const list = JSON.parse(event.data) as Array<{ uuid: string; title: string; command: string; exited: boolean; waiting?: { needle: string; since: number } | null }>
398
403
  if (!Array.isArray(list)) return
399
404
  store.reduce(s => ctx.get('betterSidebar')?.isTabEnabled('terminal') === false
400
405
  ? s
@@ -1160,6 +1165,41 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
1160
1165
  }
1161
1166
  }, [ctx, sessionId, cwd])
1162
1167
 
1168
+ /** Tree-row rename reconciliation: retarget every open tab whose path was
1169
+ * the renamed file (the editor content survives and later saves land on
1170
+ * the new path; the title follows the new base name). */
1171
+ const onPathRenamed = useCallback((oldPath: string, newPath: string): void => {
1172
+ const service = ctx.get('betterSidebar')
1173
+ if (service === undefined) return
1174
+ const snapshot = store.getSnapshot().state
1175
+ if (snapshot === undefined) return
1176
+ for (const leaf of allLeaves(snapshot.splits)) {
1177
+ for (const tab of leaf.tabs) {
1178
+ if (tab.path === oldPath) service.updateTab(tab.id, { path: newPath, title: baseName(newPath) })
1179
+ }
1180
+ }
1181
+ for (const float of snapshot.floats) {
1182
+ if (float.tab.path === oldPath) service.updateTab(float.tab.id, { path: newPath, title: baseName(newPath) })
1183
+ }
1184
+ }, [ctx, store])
1185
+
1186
+ /** Tree-row delete reconciliation: close every open tab at or under the
1187
+ * removed path (a stale tab's next save would fail against a missing
1188
+ * path). Floating tabs are as open as docked ones. */
1189
+ const onPathRemoved = useCallback((target: string): void => {
1190
+ const service = ctx.get('betterSidebar')
1191
+ if (service === undefined) return
1192
+ const snapshot = store.getSnapshot().state
1193
+ if (snapshot === undefined) return
1194
+ const tabs: SidebarTab[] = []
1195
+ for (const leaf of allLeaves(snapshot.splits)) tabs.push(...leaf.tabs)
1196
+ for (const float of snapshot.floats) tabs.push(float.tab)
1197
+ for (const tab of tabs) {
1198
+ const path = tab.path
1199
+ if (path !== undefined && (path === target || isWithinWorkspace(target, path))) service.closeTab(tab.id)
1200
+ }
1201
+ }, [ctx, store])
1202
+
1163
1203
  if (state === undefined || sessionId === undefined) {
1164
1204
  // Keep the unavailable controls focusable: touch users have no hover, so
1165
1205
  // focus is the only way the existing Tooltip can explain what is missing.
@@ -1205,6 +1245,15 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
1205
1245
  * strip must never break because a plugin's badge computation failed.
1206
1246
  */
1207
1247
  const tabBadgeOf = (tab: SidebarTab): ReactNode => {
1248
+ // Agent-terminal wait indicator (sidebar-internal, deliberately NOT a
1249
+ // TabDescriptor.badge — that API is type-keyed and shared with external
1250
+ // plugins, and cannot address one tab): the agent-terminals push mirrors
1251
+ // the model's live terminal_wait_for into state.agentWaits; an agent tab
1252
+ // whose uuid is waiting shows the hourglass pill.
1253
+ if (isAgentTabId(tab.id)) {
1254
+ const wait = state.agentWaits?.[agentUuidOf(tab.id)]
1255
+ if (wait !== undefined) return <span className={css.tabBadge}>{'⏳'}</span>
1256
+ }
1208
1257
  const descriptor = ctx.get('betterSidebar')?.getTab(tab.type)
1209
1258
  if (descriptor?.badge === undefined) return null
1210
1259
  let value: string | number | null | undefined
@@ -1250,6 +1299,8 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
1250
1299
  revealed={state.revealed ?? []}
1251
1300
  onToggleDir={(path) => { store.reduce(s => toggleExpanded(s, path)) }}
1252
1301
  onReferenceFile={referenceInChat}
1302
+ onPathRenamed={onPathRenamed}
1303
+ onPathRemoved={onPathRemoved}
1253
1304
  ctx={ctx}
1254
1305
  store={store}
1255
1306
  visible={placement === 'float' ? true : state.panelOpen && active}
@@ -47,6 +47,7 @@ import {
47
47
  shouldActivateTerminalLink,
48
48
  openTerminalUrl,
49
49
  } from './terminal-links.ts'
50
+ import { TerminalWaitBanner } from './TerminalWaitBanner.tsx'
50
51
  import css from './sidebar.module.css'
51
52
 
52
53
  /** How many consecutive unreasoned failures before showing the error banner. */
@@ -122,6 +123,27 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store:
122
123
  const [fatal, setFatal] = useState<string | null>(null)
123
124
  const [depsFatal, setDepsFatal] = useState<TerminalDepsInfo | null>(null)
124
125
  const [lastUrl, setLastUrl] = useState<string | null>(null)
126
+ // Agent terminals only: the model's active terminal_wait_for (mirrored
127
+ // from the host's agent-terminals push into the store) drives the wait
128
+ // banner. Read + subscribe like the font prefs above; the banner vanishes
129
+ // when the host's push drops the waiting field (skip / exit / abort all
130
+ // converge through the same push). getSnapshot() is {sessionId, state?,
131
+ // prefs} — the state may be briefly undefined around session switches.
132
+ const agentUuid = isAgentTabId(tabId) ? agentUuidOf(tabId) : null
133
+ const [waiting, setWaiting] = useState<{ needle: string; since: number } | undefined>(undefined)
134
+ useEffect(() => {
135
+ if (agentUuid === null) return
136
+ const read = (): void => {
137
+ const next = store.getSnapshot().state?.agentWaits?.[agentUuid]
138
+ setWaiting(prev => {
139
+ const nextValue = next === undefined ? undefined : { needle: next.needle, since: next.since }
140
+ if (prev?.needle === nextValue?.needle && prev?.since === nextValue?.since) return prev
141
+ return nextValue
142
+ })
143
+ }
144
+ read()
145
+ return store.subscribe(read)
146
+ }, [agentUuid, store])
125
147
  const connectRef = useRef<(() => void) | null>(null)
126
148
 
127
149
  useEffect(() => {
@@ -374,6 +396,12 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store:
374
396
 
375
397
  return (
376
398
  <div className={css.terminalWrap}>
399
+ {agentUuid !== null && waiting !== undefined && (
400
+ <TerminalWaitBanner
401
+ needle={waiting.needle}
402
+ onSkip={() => { void api.agentSkipWait(agentUuid).catch(() => { /* 跳过失败时 banner 留存,可重试 */ }) }}
403
+ />
404
+ )}
377
405
  {depsFatal !== null && (
378
406
  <TerminalDepsBanner deps={depsFatal} onRetry={() => { setDepsFatal(null); connectRef.current?.() }} />
379
407
  )}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The "Agent 正在等待 {needle}" wait banner: rendered at the top of an
3
+ * agent-owned terminal's view while the model blocks in terminal_wait_for.
4
+ * The skip button asks the host to abort every active wait on the terminal
5
+ * (`agent-pty.skip-wait`); the banner disappears when the host's next
6
+ * agent-terminals push drops the waiting field — no optimistic UI. Kept in
7
+ * its own module (no xterm imports) so jsdom tests can render it directly.
8
+ */
9
+ import { t } from './locales.ts'
10
+ import css from './sidebar.module.css'
11
+
12
+ /** Cap the needle shown inline; the full text rides the title tooltip. */
13
+ const NEEDLE_DISPLAY_CAP = 80
14
+
15
+ /** Truncate one needle for inline display (title attr carries the full text). */
16
+ export function truncateNeedle(needle: string): string {
17
+ return needle.length > NEEDLE_DISPLAY_CAP ? `${needle.slice(0, NEEDLE_DISPLAY_CAP - 1)}…` : needle
18
+ }
19
+
20
+ export function TerminalWaitBanner(props: { needle: string; onSkip: () => void }) {
21
+ const { needle, onSkip } = props
22
+ return (
23
+ <div className={css.terminalWaitBanner}>
24
+ <span className={css.terminalWaitNeedle} title={needle}>
25
+ {t('terminalWaitBanner', { needle: truncateNeedle(needle) })}
26
+ </span>
27
+ <button type="button" className={css.terminalRetry} onClick={onSkip}>
28
+ {t('terminalSkipWait')}
29
+ </button>
30
+ </div>
31
+ )
32
+ }
@@ -60,11 +60,14 @@ export function TreePanel(props: {
60
60
  onOpenWith?: (targetId: string, path: string) => void
61
61
  onToggleOpenWithPin?: (targetId: string) => void
62
62
  onReferenceFile: (path: string, isDir: boolean) => void
63
+ /** Tree-row mutations (passed through to the file tree; absent → hidden). */
64
+ onPathRenamed?: (oldPath: string, newPath: string) => void
65
+ onPathRemoved?: (path: string) => void
63
66
  /** Full-window presentation: the panel fills its host instead of docking
64
67
  * at a fixed width. */
65
68
  full?: boolean
66
69
  }) {
67
- const { sessionId, cwd, expanded, revealed, onToggle, onOpenFile, onOpenFileNewTab, onOpenFileSide, openWithTargets, openWithPinned, openWithSsh, onOpenWith, onToggleOpenWithPin, onReferenceFile, full } = props
70
+ const { sessionId, cwd, expanded, revealed, onToggle, onOpenFile, onOpenFileNewTab, onOpenFileSide, openWithTargets, openWithPinned, openWithSsh, onOpenWith, onToggleOpenWithPin, onReferenceFile, onPathRenamed, onPathRemoved, full } = props
68
71
  const [query, setQuery] = useState('')
69
72
  const [results, setResults] = useState<{ matches: string[]; truncated: boolean } | null>(null)
70
73
  const [error, setError] = useState<string | null>(null)
@@ -248,6 +251,8 @@ export function TreePanel(props: {
248
251
  onOpenWith={onOpenWith}
249
252
  onToggleOpenWithPin={onToggleOpenWithPin}
250
253
  onReferenceFile={onReferenceFile}
254
+ onPathRenamed={onPathRenamed}
255
+ onPathRemoved={onPathRemoved}
251
256
  refreshTick={refreshTick}
252
257
  onUploadRequest={startUpload}
253
258
  busy={busy}
package/src/client/api.ts CHANGED
@@ -212,6 +212,14 @@ export const api = {
212
212
  call<FsTextResult | FsBinaryResult>('fs.read', scopePayload(scope, { path }), signal),
213
213
  fsWrite: (scope: SessionScope, path: string, content: string) =>
214
214
  call<{ ok: true }>('fs.write', scopePayload(scope, { path, content })),
215
+ /** Rename one tree row within its directory (single-segment name; a
216
+ * destination-existence clash is a 409; symlink rows rename the link). */
217
+ fsRename: (scope: SessionScope, path: string, name: string) =>
218
+ call<{ path: string }>('fs.rename', scopePayload(scope, { path, name })),
219
+ /** Delete one tree row permanently (recursive for directories; a symlink
220
+ * row unlinks the link only). */
221
+ fsRemove: (scope: SessionScope, path: string) =>
222
+ call<{ path: string }>('fs.remove', scopePayload(scope, { path })),
215
223
  /** Upload one file's raw bytes into `dir` (keeps the folder tree via
216
224
  * `relativePath`); the host streams it under the session workspace. */
217
225
  uploadFile: (scope: SessionScope, dir: string, relativePath: string, body: Blob, signal?: AbortSignal) =>
@@ -241,6 +249,14 @@ export const api = {
241
249
  /** Full patch text of one commit (diff display for the history rows). */
242
250
  gitCommitDiff: (scope: SessionScope, hash: string, worktree?: string, signal?: AbortSignal) =>
243
251
  call<{ diff: string }>('git.commit-diff', gitPayload(scope, worktree, { hash }), signal),
252
+ /** Both sides' full file contents for a diff-fold expansion; a missing
253
+ * side is null (untracked / deleted) and the view degrades the fold. */
254
+ gitFoldContents: (scope: SessionScope, opts: { path: string; staged?: boolean; hash?: string }, worktree?: string, signal?: AbortSignal) =>
255
+ call<{ old: string | null; new: string | null }>('git.fold-contents', gitPayload(scope, worktree, {
256
+ path: opts.path,
257
+ ...(opts.staged !== undefined ? { staged: opts.staged } : {}),
258
+ ...(opts.hash !== undefined ? { hash: opts.hash } : {}),
259
+ }), signal),
244
260
  /** Discard the worktree changes of one file (the index is untouched). */
245
261
  gitDiscard: (scope: SessionScope, path: string, worktree?: string) =>
246
262
  call<{ ok: true }>('git.discard', gitPayload(scope, worktree, { path })),
@@ -258,6 +274,14 @@ export const api = {
258
274
  /** Release an agent terminal by uuid (tab closed while WS was down). */
259
275
  agentPtyClose: (uuid: string) =>
260
276
  call<{ ok: true }>('agent-pty.close', { uuid }),
277
+ /** Skip every active terminal_wait_for on one agent terminal (the wait
278
+ * banner's skip button). Idempotent: {skipped:0} when none is active. */
279
+ agentSkipWait: (uuid: string) =>
280
+ call<{ ok: true; skipped: number }>('agent-pty.skip-wait', { uuid }),
281
+ /** The session lens: file operations the model performed in one session
282
+ * (parsed from the session's own event log; newest first). */
283
+ changesOps: (scope: SessionScope, signal?: AbortSignal) =>
284
+ call<{ ops: Array<{ path: string; tool: string; time: number; count: number }> }>('changes.ops', scopePayload(scope, {}), signal),
261
285
  /** Terminal dependency status (issue #140): after a WS close 1011 with
262
286
  * reason `pty-deps-missing` the view fetches the full repair details here
263
287
  * (the close reason itself is capped at 123 bytes). */
@@ -156,6 +156,26 @@ export const ar: Record<string, string> = {
156
156
  produced: 'النواتج',
157
157
  producedOpen: 'فتح في الشريط الجانبي',
158
158
  disconnected: 'انقطع اتصال الطرفية، جارٍ إعادة الاتصال…',
159
+ terminalWaitBanner: 'الوكيل ينتظر {needle}',
160
+ terminalSkipWait: 'تخطي الانتظار',
161
+ gitFoldExpand: 'إظهار {count} سطرًا من السياق',
162
+ gitFoldLoading: 'جارٍ التوسيع…',
163
+ gitFoldFailed: 'فشل التوسيع',
164
+ rename: 'إعادة تسمية',
165
+ renameInvalid: 'لا يمكن أن يكون الاسم فارغًا أو يحتوي على فواصل مسار.',
166
+ delete: 'حذف',
167
+ deleteTitle: 'حذف "{name}"؟',
168
+ deleteDescFile: 'سيتم حذف هذا الملف نهائيًا ولا يمكن التراجع.',
169
+ deleteDescDir: 'سيتم حذف هذا الدليل ومحتواه نهائيًا ولا يمكن التراجع.',
170
+ dismiss: 'إغلاق',
171
+ changesSessionGit: 'تغييرات Git',
172
+ changesSessionLens: 'تغييرات الجلسة',
173
+ changesEmpty: 'لا عمليات ملفات في هذه الجلسة',
174
+ changesCount: '{count} عملية ملف',
175
+ changesRedacted: '[مُنقَّح]',
176
+ changesBinary: 'ملف ثنائي — لا معاينة',
177
+ changesPreviewError: 'فشل قراءة المعاينة: {message}',
178
+ changesLens: 'عرض',
159
179
  exited: 'خرجت عملية الطرفية',
160
180
  noSession: 'اختر محادثة لاستخدام الشريط الجانبي',
161
181
  pluginNotLoaded: 'الإضافة غير محمّلة؛ التبويب غير متاح:',
@@ -141,6 +141,26 @@ export const de: Record<string, string> = {
141
141
  produced: 'Erstellt',
142
142
  producedOpen: 'In der Seitenleiste öffnen',
143
143
  disconnected: 'Terminalverbindung getrennt, Verbindung wird wiederhergestellt…',
144
+ terminalWaitBanner: 'Agent wartet auf {needle}',
145
+ terminalSkipWait: 'Warten abbrechen',
146
+ gitFoldExpand: '{count} Kontextzeilen einblenden',
147
+ gitFoldLoading: 'Wird eingeblendet…',
148
+ gitFoldFailed: 'Einblenden fehlgeschlagen',
149
+ rename: 'Umbenennen',
150
+ renameInvalid: 'Der Name darf nicht leer sein und keine Pfadtrenner enthalten.',
151
+ delete: 'Löschen',
152
+ deleteTitle: '„{name}“ löschen?',
153
+ deleteDescFile: 'Diese Datei wird endgültig gelöscht. Dies kann nicht rückgängig gemacht werden.',
154
+ deleteDescDir: 'Dieses Verzeichnis und sein gesamter Inhalt werden endgültig gelöscht. Dies kann nicht rückgängig gemacht werden.',
155
+ dismiss: 'Schließen',
156
+ changesSessionGit: 'Git-Änderungen',
157
+ changesSessionLens: 'Sitzungsänderungen',
158
+ changesEmpty: 'Keine Dateioperationen in dieser Sitzung',
159
+ changesCount: '{count} Dateioperationen',
160
+ changesRedacted: '[GESCHWÄRZT]',
161
+ changesBinary: 'Binärdatei – keine Vorschau',
162
+ changesPreviewError: 'Vorschau konnte nicht gelesen werden: {message}',
163
+ changesLens: 'Ansicht',
144
164
  exited: 'Terminalprozess beendet',
145
165
  noSession: 'Wählen Sie eine Sitzung, um die Seitenleiste zu verwenden',
146
166
  pluginNotLoaded: 'Plugin nicht geladen; Tab vorübergehend nicht verfügbar:',
@@ -148,6 +148,26 @@ export const fr: Record<string, string> = {
148
148
  produced: 'Produits de cette exécution',
149
149
  producedOpen: 'Ouvrir dans la barre latérale',
150
150
  disconnected: 'Connexion du terminal perdue, reconnexion…',
151
+ terminalWaitBanner: 'L’agent attend {needle}',
152
+ terminalSkipWait: 'Ignorer l’attente',
153
+ gitFoldExpand: 'Afficher les {count} lignes de contexte',
154
+ gitFoldLoading: 'Déploiement…',
155
+ gitFoldFailed: 'Impossible de déployer',
156
+ rename: 'Renommer',
157
+ renameInvalid: 'Le nom ne peut pas être vide ni contenir de séparateur de chemin.',
158
+ delete: 'Supprimer',
159
+ deleteTitle: 'Supprimer « {name} » ?',
160
+ deleteDescFile: 'Ce fichier sera définitivement supprimé. Action irréversible.',
161
+ deleteDescDir: 'Ce répertoire et tout son contenu seront définitivement supprimés. Action irréversible.',
162
+ dismiss: 'Fermer',
163
+ changesSessionGit: 'Modifications Git',
164
+ changesSessionLens: 'Modifications de session',
165
+ changesEmpty: 'Aucune opération de fichier dans cette session',
166
+ changesCount: '{count} opérations de fichier',
167
+ changesRedacted: '[MASQUÉ]',
168
+ changesBinary: 'Fichier binaire — pas d’aperçu',
169
+ changesPreviewError: 'Échec de lecture de l’aperçu : {message}',
170
+ changesLens: 'Vue',
151
171
  exited: 'Le processus du terminal s’est terminé',
152
172
  noSession: 'Sélectionnez une session pour utiliser la barre latérale',
153
173
  pluginNotLoaded: 'Plugin non chargé, onglet indisponible pour le moment :',
@@ -155,6 +155,26 @@ export const hi: Record<string, string> = {
155
155
  produced: 'उत्पादित',
156
156
  producedOpen: 'साइडबार में खोलें',
157
157
  disconnected: 'टर्मिनल डिस्कनेक्ट हो गया, पुनः कनेक्ट हो रहा…',
158
+ terminalWaitBanner: 'एजेंट {needle} की प्रतीक्षा कर रहा है',
159
+ terminalSkipWait: 'प्रतीक्षा छोड़ें',
160
+ gitFoldExpand: '{count} संदर्भ पंक्तियाँ दिखाएँ',
161
+ gitFoldLoading: 'खोल रहे हैं…',
162
+ gitFoldFailed: 'खोलने में विफल',
163
+ rename: 'नाम बदलें',
164
+ renameInvalid: 'नाम खाली नहीं हो सकता या पाथ सेपरेटर नहीं हो सकता।',
165
+ delete: 'हटाएँ',
166
+ deleteTitle: '"{name}" हटाएँ?',
167
+ deleteDescFile: 'यह फ़ाइल स्थायी रूप से हट जाएगी। इसे वापस नहीं किया जा सकता।',
168
+ deleteDescDir: 'यह निर्देशिका और उसकी सामग्री स्थायी रूप से हट जाएगी। इसे वापस नहीं किया जा सकता।',
169
+ dismiss: 'बंद करें',
170
+ changesSessionGit: 'Git बदलाव',
171
+ changesSessionLens: 'सेशन बदलाव',
172
+ changesEmpty: 'इस सेशन में कोई फ़ाइल ऑपरेशन नहीं',
173
+ changesCount: '{count} फ़ाइल ऑपरेशन',
174
+ changesRedacted: '[मास्क किया गया]',
175
+ changesBinary: 'बाइनरी फ़ाइल — कोई पूर्वावलोकन नहीं',
176
+ changesPreviewError: 'पूर्वावलोकन पढ़ने में विफल: {message}',
177
+ changesLens: 'दृश्य',
158
178
  exited: 'टर्मिनल प्रक्रिया बाहर निकली',
159
179
  noSession: 'साइडबार उपयोग करने के लिए एक वार्तालाप चुनें',
160
180
  pluginNotLoaded: 'प्लगइन लोड नहीं; टैब अनुपलब्ध:',
@@ -153,6 +153,26 @@ export const id: Record<string, string> = {
153
153
  produced: 'Dihasilkan',
154
154
  producedOpen: 'Buka di sidebar',
155
155
  disconnected: 'Terminal terputus, menyambung ulang…',
156
+ terminalWaitBanner: 'Agen menunggu {needle}',
157
+ terminalSkipWait: 'Lewati penungguan',
158
+ gitFoldExpand: 'Tampilkan {count} baris konteks',
159
+ gitFoldLoading: 'Membentang…',
160
+ gitFoldFailed: 'Gagal membentang',
161
+ rename: 'Ganti nama',
162
+ renameInvalid: 'Nama tidak boleh kosong atau mengandung pemisah jalur.',
163
+ delete: 'Hapus',
164
+ deleteTitle: 'Hapus "{name}"?',
165
+ deleteDescFile: 'File ini dihapus permanen dan tidak dapat dibatalkan.',
166
+ deleteDescDir: 'Direktori ini dan seluruh isinya dihapus permanen dan tidak dapat dibatalkan.',
167
+ dismiss: 'Tutup',
168
+ changesSessionGit: 'Perubahan Git',
169
+ changesSessionLens: 'Perubahan sesi',
170
+ changesEmpty: 'Tidak ada operasi file di sesi ini',
171
+ changesCount: '{count} operasi file',
172
+ changesRedacted: '[DISENSOR]',
173
+ changesBinary: 'File biner — tanpa pratinjau',
174
+ changesPreviewError: 'Gagal membaca pratinjau: {message}',
175
+ changesLens: 'Tampilan',
156
176
  exited: 'Proses terminal keluar',
157
177
  noSession: 'Pilih obrolan untuk menggunakan sidebar',
158
178
  pluginNotLoaded: 'Plugin tidak dimuat; tab tidak tersedia untuk sementara:',
@@ -146,6 +146,26 @@ export const it: Record<string, string> = {
146
146
  produced: 'Prodotti',
147
147
  producedOpen: 'Apri nella barra laterale',
148
148
  disconnected: 'Terminale disconnesso, riconnessione…',
149
+ terminalWaitBanner: 'L’agente sta attendendo {needle}',
150
+ terminalSkipWait: 'Salta attesa',
151
+ gitFoldExpand: 'Mostra le {count} righe di contesto',
152
+ gitFoldLoading: 'Espansione…',
153
+ gitFoldFailed: 'Impossibile espandere',
154
+ rename: 'Rinomina',
155
+ renameInvalid: 'Il nome non può essere vuoto né contenere separatori di percorso.',
156
+ delete: 'Elimina',
157
+ deleteTitle: 'Eliminare "{name}"?',
158
+ deleteDescFile: 'Il file verrà eliminato definitivamente. Operazione irreversibile.',
159
+ deleteDescDir: 'La directory e tutto il suo contenuto verranno eliminati definitivamente. Operazione irreversibile.',
160
+ dismiss: 'Chiudi',
161
+ changesSessionGit: 'Modifiche Git',
162
+ changesSessionLens: 'Modifiche di sessione',
163
+ changesEmpty: 'Nessuna operazione su file in questa sessione',
164
+ changesCount: '{count} operazioni su file',
165
+ changesRedacted: '[OSCURATO]',
166
+ changesBinary: 'File binario — nessuna anteprima',
167
+ changesPreviewError: 'Lettura anteprima non riuscita: {message}',
168
+ changesLens: 'Vista',
149
169
  exited: 'Il processo del terminale è terminato',
150
170
  noSession: 'Selezioni una conversazione per usare la barra laterale',
151
171
  pluginNotLoaded: 'Plugin non caricato; scheda non disponibile:',
@@ -155,6 +155,26 @@ export const ja: Record<string, string> = {
155
155
  produced: '今回の産物',
156
156
  producedOpen: 'サイドバーで開く',
157
157
  disconnected: 'ターミナル接続が切れました、再接続中…',
158
+ terminalWaitBanner: 'エージェントが {needle} を待機中',
159
+ terminalSkipWait: '待機をスキップ',
160
+ gitFoldExpand: 'コンテキスト {count} 行を表示',
161
+ gitFoldLoading: '展開中…',
162
+ gitFoldFailed: '展開できませんでした',
163
+ rename: '名前を変更',
164
+ renameInvalid: '名前は空にできないか、パス区切りを含めてはいけません。',
165
+ delete: '削除',
166
+ deleteTitle: '「{name}」を削除しますか?',
167
+ deleteDescFile: 'このファイルは完全に削除されます。元に戻せません。',
168
+ deleteDescDir: 'このディレクトリとその内容は完全に削除されます。元に戻せません。',
169
+ dismiss: '閉じる',
170
+ changesSessionGit: 'Git 変更',
171
+ changesSessionLens: 'セッション変更',
172
+ changesEmpty: 'このセッションにはファイル操作がありません',
173
+ changesCount: '{count} 件のファイル操作',
174
+ changesRedacted: '[マスク済み]',
175
+ changesBinary: 'バイナリファイルのためプレビューできません',
176
+ changesPreviewError: 'プレビューの読み取りに失敗: {message}',
177
+ changesLens: 'ビュー',
158
178
  exited: 'ターミナルプロセスが終了しました',
159
179
  noSession: 'サイドバーを使うには会話を選択してください',
160
180
  pluginNotLoaded: 'プラグイン未読み込み、タブは一時的に利用不可:',
@@ -147,6 +147,26 @@ export const ko: Record<string, string> = {
147
147
  produced: '이번 산출물',
148
148
  producedOpen: '사이드바에서 열기',
149
149
  disconnected: '터미널 연결이 끊겨 다시 연결하는 중…',
150
+ terminalWaitBanner: '에이전트가 {needle} 대기 중',
151
+ terminalSkipWait: '대기 건너뛰기',
152
+ gitFoldExpand: '컨텍스트 {count}줄 보기',
153
+ gitFoldLoading: '펼치는 중…',
154
+ gitFoldFailed: '펼치기 실패',
155
+ rename: '이름 바꾸기',
156
+ renameInvalid: '이름은 비어 있거나 경로 구분자를 포함할 수 없습니다.',
157
+ delete: '삭제',
158
+ deleteTitle: '"{name}"을(를) 삭제하시겠습니까?',
159
+ deleteDescFile: '이 파일은 영구 삭제되며 되돌릴 수 없습니다.',
160
+ deleteDescDir: '이 디렉터리와 그 내용이 영구 삭제되며 되돌릴 수 없습니다.',
161
+ dismiss: '닫기',
162
+ changesSessionGit: 'Git 변경',
163
+ changesSessionLens: '세션 변경',
164
+ changesEmpty: '이 세션에는 파일 작업이 없습니다',
165
+ changesCount: '파일 작업 {count}건',
166
+ changesRedacted: '[마스킹됨]',
167
+ changesBinary: '바이너리 파일 — 미리보기 없음',
168
+ changesPreviewError: '미리보기 읽기 실패: {message}',
169
+ changesLens: '보기',
150
170
  exited: '터미널 프로세스가 종료되었습니다',
151
171
  noSession: '사이드바를 사용하려면 대화를 선택하세요',
152
172
  pluginNotLoaded: '플러그인이 로드되지 않아 탭을 지금 사용할 수 없습니다:',
@@ -153,6 +153,26 @@ export const nl: Record<string, string> = {
153
153
  produced: 'Geproduceerd',
154
154
  producedOpen: 'Openen in zijbalk',
155
155
  disconnected: 'Terminalverbinding verbroken, opnieuw verbinden…',
156
+ terminalWaitBanner: 'Agent wacht op {needle}',
157
+ terminalSkipWait: 'Wachttijd overslaan',
158
+ gitFoldExpand: 'Toon {count} contextregels',
159
+ gitFoldLoading: 'Uitvouwen…',
160
+ gitFoldFailed: 'Uitvouwen mislukt',
161
+ rename: 'Naam wijzigen',
162
+ renameInvalid: 'De naam mag niet leeg zijn of padscheiders bevatten.',
163
+ delete: 'Verwijderen',
164
+ deleteTitle: '"{name}" verwijderen?',
165
+ deleteDescFile: 'Dit verwijdert het bestand definitief. Dit kan niet ongedaan worden gemaakt.',
166
+ deleteDescDir: 'Dit verwijdert de map en de volledige inhoud definitief. Dit kan niet ongedaan worden gemaakt.',
167
+ dismiss: 'Sluiten',
168
+ changesSessionGit: 'Git-wijzigingen',
169
+ changesSessionLens: 'Sessiewijzigingen',
170
+ changesEmpty: 'Geen bestandsbewerkingen in deze sessie',
171
+ changesCount: '{count} bestandsbewerkingen',
172
+ changesRedacted: '[GEREDICEERD]',
173
+ changesBinary: 'Binair bestand — geen voorbeeldweergave',
174
+ changesPreviewError: 'Voorbeeldweergave lezen mislukt: {message}',
175
+ changesLens: 'Weergave',
156
176
  exited: 'Terminalproces beëindigd',
157
177
  noSession: 'Selecteer een conversatie om de zijbalk te gebruiken',
158
178
  pluginNotLoaded: 'Plugin niet geladen; tabblad niet beschikbaar:',
@@ -157,6 +157,26 @@ export const pl: Record<string, string> = {
157
157
  produced: 'Wyprodukowane',
158
158
  producedOpen: 'Otwórz w panelu bocznym',
159
159
  disconnected: 'Terminal odłączony, ponowne łączenie…',
160
+ terminalWaitBanner: 'Agent czeka na {needle}',
161
+ terminalSkipWait: 'Pomiń oczekiwanie',
162
+ gitFoldExpand: 'Pokaż {count} linii kontekstu',
163
+ gitFoldLoading: 'Rozwijanie…',
164
+ gitFoldFailed: 'Nie udało się rozwinąć',
165
+ rename: 'Zmień nazwę',
166
+ renameInvalid: 'Nazwa nie może być pusta ani zawierać separatorów ścieżki.',
167
+ delete: 'Usuń',
168
+ deleteTitle: 'Usunąć „{name}"?',
169
+ deleteDescFile: 'Ten plik zostanie trwale usunięty. Nie można tego cofnąć.',
170
+ deleteDescDir: 'Ten katalog i cała jego zawartość zostaną trwale usunięte. Nie można tego cofnąć.',
171
+ dismiss: 'Zamknij',
172
+ changesSessionGit: 'Zmiany Git',
173
+ changesSessionLens: 'Zmiany sesji',
174
+ changesEmpty: 'Brak operacji na plikach w tej sesji',
175
+ changesCount: 'Operacji na plikach: {count}',
176
+ changesRedacted: '[ZREDAKOWANO]',
177
+ changesBinary: 'Plik binarny — brak podglądu',
178
+ changesPreviewError: 'Nie udało się odczytać podglądu: {message}',
179
+ changesLens: 'Widok',
160
180
  exited: 'Proces terminala zakończony',
161
181
  noSession: 'Wybierz rozmowę, aby korzystać z panelu bocznego',
162
182
  pluginNotLoaded: 'Wtyczka niezaładowana; karta chwilowo niedostępna:',