dsh-coding-sidebar 1.0.9 → 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.
- package/README.md +2 -2
- package/lib/client-editor.js +215 -184
- package/lib/client-registry.js +504 -309
- package/lib/client-terminal.js +210 -179
- package/lib/client.js +507 -312
- package/lib/index.js +63 -0
- package/lib/types/changes-ops.d.ts +31 -0
- package/lib/types/client/SessionLens.d.ts +4 -0
- package/lib/types/client/api.d.ts +10 -0
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/redact.d.ts +14 -0
- package/package.json +1 -1
- package/src/changes-ops.ts +78 -0
- package/src/client/GitView.tsx +37 -0
- package/src/client/SessionLens.tsx +95 -0
- package/src/client/api.ts +4 -0
- package/src/client/locales-ar.ts +8 -0
- package/src/client/locales-de.ts +8 -0
- package/src/client/locales-fr.ts +8 -0
- package/src/client/locales-hi.ts +8 -0
- package/src/client/locales-id.ts +8 -0
- package/src/client/locales-it.ts +8 -0
- package/src/client/locales-ja.ts +8 -0
- package/src/client/locales-ko.ts +8 -0
- package/src/client/locales-nl.ts +8 -0
- package/src/client/locales-pl.ts +8 -0
- package/src/client/locales-pt.ts +8 -0
- package/src/client/locales-ru.ts +8 -0
- package/src/client/locales-sv.ts +8 -0
- package/src/client/locales-th.ts +8 -0
- package/src/client/locales-tr.ts +8 -0
- package/src/client/locales-vi.ts +8 -0
- package/src/client/locales-zh-HK.ts +8 -0
- package/src/client/locales-zh-MO.ts +8 -0
- package/src/client/locales-zh-TW.ts +8 -0
- package/src/client/locales.ts +16 -0
- package/src/client/redact.ts +39 -0
- package/src/client/sidebar.module.css +115 -0
- package/src/index.ts +13 -0
package/lib/index.js
CHANGED
|
@@ -532,6 +532,64 @@ async function removeWorkspaceEntry(input) {
|
|
|
532
532
|
return { path: absolute };
|
|
533
533
|
}
|
|
534
534
|
//#endregion
|
|
535
|
+
//#region src/changes-ops.ts
|
|
536
|
+
/** Argument keys a file-addressing tool may use for its target path. */
|
|
537
|
+
const PATH_KEYS = [
|
|
538
|
+
"path",
|
|
539
|
+
"file_path",
|
|
540
|
+
"filePath",
|
|
541
|
+
"notebook_path",
|
|
542
|
+
"filename"
|
|
543
|
+
];
|
|
544
|
+
/**
|
|
545
|
+
* Whether a tool name looks like it MUTATES files. Deliberately coarse
|
|
546
|
+
* (substring match on the mutating verbs) so host-side and plugin-side
|
|
547
|
+
* file tools both qualify; read-only tools never match.
|
|
548
|
+
*/
|
|
549
|
+
function isWriteTool(name) {
|
|
550
|
+
const lowered = name.toLowerCase();
|
|
551
|
+
return /write|edit|patch|apply|create_file|insert/.test(lowered);
|
|
552
|
+
}
|
|
553
|
+
/** Extract the addressed path from one tool call's arguments JSON. */
|
|
554
|
+
function argumentPath(args) {
|
|
555
|
+
if (args === "") return void 0;
|
|
556
|
+
try {
|
|
557
|
+
const parsed = JSON.parse(args);
|
|
558
|
+
for (const key of PATH_KEYS) {
|
|
559
|
+
const value = parsed[key];
|
|
560
|
+
if (typeof value === "string" && value !== "") return value;
|
|
561
|
+
}
|
|
562
|
+
} catch {}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Fold a session event log into the deduplicated file-operation list,
|
|
566
|
+
* newest first. `tool/call` events with a mutating tool name and an
|
|
567
|
+
* addressable path are collected; every path keeps only its latest call
|
|
568
|
+
* (plus a touch count). Rows outside a live sessions registry read come
|
|
569
|
+
* back as an empty list — the page degrades to the empty state.
|
|
570
|
+
* @param events - the session's append-only event log (oldest → newest).
|
|
571
|
+
*/
|
|
572
|
+
function sessionFileOps(events) {
|
|
573
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
574
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
575
|
+
const event = events[index];
|
|
576
|
+
if (event === void 0 || event.type !== "tool/call") continue;
|
|
577
|
+
const name = typeof event.data.name === "string" ? event.data.name : "";
|
|
578
|
+
if (name === "" || !isWriteTool(name)) continue;
|
|
579
|
+
const path = argumentPath(typeof event.data.arguments === "string" ? event.data.arguments : "");
|
|
580
|
+
if (path === void 0) continue;
|
|
581
|
+
const existing = byPath.get(path);
|
|
582
|
+
if (existing === void 0) byPath.set(path, {
|
|
583
|
+
path,
|
|
584
|
+
tool: name,
|
|
585
|
+
time: event.time,
|
|
586
|
+
count: 1
|
|
587
|
+
});
|
|
588
|
+
else existing.count += 1;
|
|
589
|
+
}
|
|
590
|
+
return [...byPath.values()].sort((left, right) => right.time - left.time);
|
|
591
|
+
}
|
|
592
|
+
//#endregion
|
|
535
593
|
//#region src/fs-search.ts
|
|
536
594
|
/**
|
|
537
595
|
* Recursive file-name search for the editor's merged-mode side panel.
|
|
@@ -4366,6 +4424,11 @@ function buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, ge
|
|
|
4366
4424
|
"terminal.deps": () => depsStatus(),
|
|
4367
4425
|
"jobs.output": (payload) => jobsApi.output(payload),
|
|
4368
4426
|
"jobs.kill": (payload) => jobsApi.kill(payload),
|
|
4427
|
+
"changes.ops": async (payload) => {
|
|
4428
|
+
const sessionId = requireString(payload, "sessionId");
|
|
4429
|
+
const stored = ctx.sessions.get(sessionId);
|
|
4430
|
+
return { ops: sessionFileOps(stored?.snapshotEvents !== void 0 ? stored.snapshotEvents() : []) };
|
|
4431
|
+
},
|
|
4369
4432
|
"subagents.live": (payload) => subagentLiveApi.live(payload),
|
|
4370
4433
|
"shell.get": () => ({
|
|
4371
4434
|
shell: terminalShell,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure derivation of the "session lens": the file operations the model
|
|
3
|
+
* performed in one session, parsed from the session's own event log (the
|
|
4
|
+
* same durable log the side chat reads — nothing here touches the host
|
|
5
|
+
* registry or the model's cursors). Kept framework-free so the parser is
|
|
6
|
+
* unit-testable in the node environment.
|
|
7
|
+
*/
|
|
8
|
+
import type { SidebarSessionEvent } from './context-types.ts';
|
|
9
|
+
/**
|
|
10
|
+
* One deduplicated file operation: the LATEST write-shaped tool call that
|
|
11
|
+
* touched `path` (earlier calls to the same file fold into it).
|
|
12
|
+
*/
|
|
13
|
+
export interface SessionFileOp {
|
|
14
|
+
/** The file path as the tool call addressed it (verbatim). */
|
|
15
|
+
path: string;
|
|
16
|
+
/** The tool that performed the latest operation (e.g. write_file). */
|
|
17
|
+
tool: string;
|
|
18
|
+
/** Epoch ms of the event. */
|
|
19
|
+
time: number;
|
|
20
|
+
/** How many write-shaped calls touched this path in total. */
|
|
21
|
+
count: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fold a session event log into the deduplicated file-operation list,
|
|
25
|
+
* newest first. `tool/call` events with a mutating tool name and an
|
|
26
|
+
* addressable path are collected; every path keeps only its latest call
|
|
27
|
+
* (plus a touch count). Rows outside a live sessions registry read come
|
|
28
|
+
* back as an empty list — the page degrades to the empty state.
|
|
29
|
+
* @param events - the session's append-only event log (oldest → newest).
|
|
30
|
+
*/
|
|
31
|
+
export declare function sessionFileOps(events: readonly SidebarSessionEvent[]): SessionFileOp[];
|
|
@@ -219,6 +219,16 @@ export declare const api: {
|
|
|
219
219
|
ok: true;
|
|
220
220
|
skipped: number;
|
|
221
221
|
}>;
|
|
222
|
+
/** The session lens: file operations the model performed in one session
|
|
223
|
+
* (parsed from the session's own event log; newest first). */
|
|
224
|
+
changesOps: (scope: SessionScope, signal?: AbortSignal) => Promise<{
|
|
225
|
+
ops: Array<{
|
|
226
|
+
path: string;
|
|
227
|
+
tool: string;
|
|
228
|
+
time: number;
|
|
229
|
+
count: number;
|
|
230
|
+
}>;
|
|
231
|
+
}>;
|
|
222
232
|
/** Terminal dependency status (issue #140): after a WS close 1011 with
|
|
223
233
|
* reason `pty-deps-missing` the view fetches the full repair details here
|
|
224
234
|
* (the close reason itself is capped at 123 bytes). */
|
|
@@ -171,6 +171,14 @@ export declare const zh: {
|
|
|
171
171
|
deleteDescFile: string;
|
|
172
172
|
deleteDescDir: string;
|
|
173
173
|
dismiss: string;
|
|
174
|
+
changesSessionGit: string;
|
|
175
|
+
changesSessionLens: string;
|
|
176
|
+
changesEmpty: string;
|
|
177
|
+
changesCount: string;
|
|
178
|
+
changesRedacted: string;
|
|
179
|
+
changesBinary: string;
|
|
180
|
+
changesPreviewError: string;
|
|
181
|
+
changesLens: string;
|
|
174
182
|
exited: string;
|
|
175
183
|
noSession: string;
|
|
176
184
|
pluginNotLoaded: string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret redaction for the session lens' file preview: heuristic masking of
|
|
3
|
+
* credential-shaped strings (API keys, bearer tokens, private key blocks,
|
|
4
|
+
* password assignments) before file content is shown in the sidebar. This
|
|
5
|
+
* layer applies ONLY to the session lens' preview pane — ordinary file reads
|
|
6
|
+
* (editor, untracked diff fallback) never pass through it, so ordinary
|
|
7
|
+
* files' content is untouched.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Mask credential-shaped strings in `text`. Best-effort by design: the goal
|
|
11
|
+
* is to keep the common accident (a key echoed into a file the model wrote)
|
|
12
|
+
* out of the sidebar, not to parse every secret format ever shipped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function redactSecrets(text: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-coding-sidebar",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes the betterSidebar service for other plugins to register sidebar tabs and file viewers. KCoder-maintained fork of DSH-better-sidebar 0.17.2 (bottom panel removed, upstream-decoupled release line).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure derivation of the "session lens": the file operations the model
|
|
3
|
+
* performed in one session, parsed from the session's own event log (the
|
|
4
|
+
* same durable log the side chat reads — nothing here touches the host
|
|
5
|
+
* registry or the model's cursors). Kept framework-free so the parser is
|
|
6
|
+
* unit-testable in the node environment.
|
|
7
|
+
*/
|
|
8
|
+
import type { SidebarSessionEvent } from './context-types.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One deduplicated file operation: the LATEST write-shaped tool call that
|
|
12
|
+
* touched `path` (earlier calls to the same file fold into it).
|
|
13
|
+
*/
|
|
14
|
+
export interface SessionFileOp {
|
|
15
|
+
/** The file path as the tool call addressed it (verbatim). */
|
|
16
|
+
path: string
|
|
17
|
+
/** The tool that performed the latest operation (e.g. write_file). */
|
|
18
|
+
tool: string
|
|
19
|
+
/** Epoch ms of the event. */
|
|
20
|
+
time: number
|
|
21
|
+
/** How many write-shaped calls touched this path in total. */
|
|
22
|
+
count: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Argument keys a file-addressing tool may use for its target path. */
|
|
26
|
+
const PATH_KEYS = ['path', 'file_path', 'filePath', 'notebook_path', 'filename'] as const
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether a tool name looks like it MUTATES files. Deliberately coarse
|
|
30
|
+
* (substring match on the mutating verbs) so host-side and plugin-side
|
|
31
|
+
* file tools both qualify; read-only tools never match.
|
|
32
|
+
*/
|
|
33
|
+
function isWriteTool(name: string): boolean {
|
|
34
|
+
const lowered = name.toLowerCase()
|
|
35
|
+
return /write|edit|patch|apply|create_file|insert/.test(lowered)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Extract the addressed path from one tool call's arguments JSON. */
|
|
39
|
+
function argumentPath(args: string): string | undefined {
|
|
40
|
+
if (args === '') return undefined
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(args) as Record<string, unknown>
|
|
43
|
+
for (const key of PATH_KEYS) {
|
|
44
|
+
const value = parsed[key]
|
|
45
|
+
if (typeof value === 'string' && value !== '') return value
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Malformed arguments JSON: not a file op we can attribute.
|
|
49
|
+
}
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fold a session event log into the deduplicated file-operation list,
|
|
55
|
+
* newest first. `tool/call` events with a mutating tool name and an
|
|
56
|
+
* addressable path are collected; every path keeps only its latest call
|
|
57
|
+
* (plus a touch count). Rows outside a live sessions registry read come
|
|
58
|
+
* back as an empty list — the page degrades to the empty state.
|
|
59
|
+
* @param events - the session's append-only event log (oldest → newest).
|
|
60
|
+
*/
|
|
61
|
+
export function sessionFileOps(events: readonly SidebarSessionEvent[]): SessionFileOp[] {
|
|
62
|
+
const byPath = new Map<string, SessionFileOp>()
|
|
63
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
64
|
+
const event = events[index]
|
|
65
|
+
if (event === undefined || event.type !== 'tool/call') continue
|
|
66
|
+
const name = typeof event.data.name === 'string' ? event.data.name : ''
|
|
67
|
+
if (name === '' || !isWriteTool(name)) continue
|
|
68
|
+
const path = argumentPath(typeof event.data.arguments === 'string' ? event.data.arguments : '')
|
|
69
|
+
if (path === undefined) continue
|
|
70
|
+
const existing = byPath.get(path)
|
|
71
|
+
if (existing === undefined) {
|
|
72
|
+
byPath.set(path, { path, tool: name, time: event.time, count: 1 })
|
|
73
|
+
} else {
|
|
74
|
+
existing.count += 1
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [...byPath.values()].sort((left, right) => right.time - left.time)
|
|
78
|
+
}
|
package/src/client/GitView.tsx
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* file changes appear without a manual refresh.
|
|
11
11
|
*/
|
|
12
12
|
import { useCallback, useEffect, useRef, useState, type MouseEvent, type ReactNode } from 'react'
|
|
13
|
+
import clsx from 'clsx'
|
|
14
|
+
import { SessionLens } from './SessionLens.tsx'
|
|
13
15
|
import {
|
|
14
16
|
Button, IconBranchOutline16, IconCodeOutline16, IconCopyOutline16, IconRefreshOutline16,
|
|
15
17
|
IconTrashOutline16, Input, Menu, Modal, writeClipboard,
|
|
@@ -90,6 +92,31 @@ export function GitView(props: {
|
|
|
90
92
|
visible: boolean
|
|
91
93
|
}) {
|
|
92
94
|
const { scope, onOpenFile, onOpenDiff, visible } = props
|
|
95
|
+
// The unified changes tab's dual lens: the git view (this component's
|
|
96
|
+
// classic body) and the session lens (what the model wrote this session).
|
|
97
|
+
// Session-selected by default? No — git is the everyday surface; the lens
|
|
98
|
+
// is one click away.
|
|
99
|
+
const [lens, setLens] = useState<'git' | 'session'>('git')
|
|
100
|
+
const lensToggle = (
|
|
101
|
+
<div className={css.changesLensToggle} role="tablist" aria-label={t('changesLens')}>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className={clsx(css.changesLensTab, lens === 'git' && css.changesLensTabActive)}
|
|
105
|
+
aria-pressed={lens === 'git'}
|
|
106
|
+
onClick={() => { setLens('git') }}
|
|
107
|
+
>
|
|
108
|
+
{t('changesSessionGit')}
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
className={clsx(css.changesLensTab, lens === 'session' && css.changesLensTabActive)}
|
|
113
|
+
aria-pressed={lens === 'session'}
|
|
114
|
+
onClick={() => { setLens('session') }}
|
|
115
|
+
>
|
|
116
|
+
{t('changesSessionLens')}
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
93
120
|
const [status, setStatus] = useState<GitStatusResult | null>(null)
|
|
94
121
|
const [worktrees, setWorktrees] = useState<GitWorktree[]>([])
|
|
95
122
|
const [selectedWorktree, setSelectedWorktree] = useState<string | undefined>()
|
|
@@ -411,8 +438,18 @@ export function GitView(props: {
|
|
|
411
438
|
)
|
|
412
439
|
}
|
|
413
440
|
|
|
441
|
+
if (lens === 'session') {
|
|
442
|
+
return (
|
|
443
|
+
<div className={css.git}>
|
|
444
|
+
{lensToggle}
|
|
445
|
+
<SessionLens scope={scope} />
|
|
446
|
+
</div>
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
414
450
|
return (
|
|
415
451
|
<div className={css.git}>
|
|
452
|
+
{lensToggle}
|
|
416
453
|
{worktrees.length > 1 && (
|
|
417
454
|
<div className={css.gitWorktreeRow}>
|
|
418
455
|
<span className={css.gitWorktreeLabel}>{t('worktree')}</span>
|
|
@@ -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
|
+
}
|
package/src/client/api.ts
CHANGED
|
@@ -278,6 +278,10 @@ export const api = {
|
|
|
278
278
|
* banner's skip button). Idempotent: {skipped:0} when none is active. */
|
|
279
279
|
agentSkipWait: (uuid: string) =>
|
|
280
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),
|
|
281
285
|
/** Terminal dependency status (issue #140): after a WS close 1011 with
|
|
282
286
|
* reason `pty-deps-missing` the view fetches the full repair details here
|
|
283
287
|
* (the close reason itself is capped at 123 bytes). */
|
package/src/client/locales-ar.ts
CHANGED
|
@@ -168,6 +168,14 @@ export const ar: Record<string, string> = {
|
|
|
168
168
|
deleteDescFile: 'سيتم حذف هذا الملف نهائيًا ولا يمكن التراجع.',
|
|
169
169
|
deleteDescDir: 'سيتم حذف هذا الدليل ومحتواه نهائيًا ولا يمكن التراجع.',
|
|
170
170
|
dismiss: 'إغلاق',
|
|
171
|
+
changesSessionGit: 'تغييرات Git',
|
|
172
|
+
changesSessionLens: 'تغييرات الجلسة',
|
|
173
|
+
changesEmpty: 'لا عمليات ملفات في هذه الجلسة',
|
|
174
|
+
changesCount: '{count} عملية ملف',
|
|
175
|
+
changesRedacted: '[مُنقَّح]',
|
|
176
|
+
changesBinary: 'ملف ثنائي — لا معاينة',
|
|
177
|
+
changesPreviewError: 'فشل قراءة المعاينة: {message}',
|
|
178
|
+
changesLens: 'عرض',
|
|
171
179
|
exited: 'خرجت عملية الطرفية',
|
|
172
180
|
noSession: 'اختر محادثة لاستخدام الشريط الجانبي',
|
|
173
181
|
pluginNotLoaded: 'الإضافة غير محمّلة؛ التبويب غير متاح:',
|
package/src/client/locales-de.ts
CHANGED
|
@@ -153,6 +153,14 @@ export const de: Record<string, string> = {
|
|
|
153
153
|
deleteDescFile: 'Diese Datei wird endgültig gelöscht. Dies kann nicht rückgängig gemacht werden.',
|
|
154
154
|
deleteDescDir: 'Dieses Verzeichnis und sein gesamter Inhalt werden endgültig gelöscht. Dies kann nicht rückgängig gemacht werden.',
|
|
155
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',
|
|
156
164
|
exited: 'Terminalprozess beendet',
|
|
157
165
|
noSession: 'Wählen Sie eine Sitzung, um die Seitenleiste zu verwenden',
|
|
158
166
|
pluginNotLoaded: 'Plugin nicht geladen; Tab vorübergehend nicht verfügbar:',
|
package/src/client/locales-fr.ts
CHANGED
|
@@ -160,6 +160,14 @@ export const fr: Record<string, string> = {
|
|
|
160
160
|
deleteDescFile: 'Ce fichier sera définitivement supprimé. Action irréversible.',
|
|
161
161
|
deleteDescDir: 'Ce répertoire et tout son contenu seront définitivement supprimés. Action irréversible.',
|
|
162
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',
|
|
163
171
|
exited: 'Le processus du terminal s’est terminé',
|
|
164
172
|
noSession: 'Sélectionnez une session pour utiliser la barre latérale',
|
|
165
173
|
pluginNotLoaded: 'Plugin non chargé, onglet indisponible pour le moment :',
|
package/src/client/locales-hi.ts
CHANGED
|
@@ -167,6 +167,14 @@ export const hi: Record<string, string> = {
|
|
|
167
167
|
deleteDescFile: 'यह फ़ाइल स्थायी रूप से हट जाएगी। इसे वापस नहीं किया जा सकता।',
|
|
168
168
|
deleteDescDir: 'यह निर्देशिका और उसकी सामग्री स्थायी रूप से हट जाएगी। इसे वापस नहीं किया जा सकता।',
|
|
169
169
|
dismiss: 'बंद करें',
|
|
170
|
+
changesSessionGit: 'Git बदलाव',
|
|
171
|
+
changesSessionLens: 'सेशन बदलाव',
|
|
172
|
+
changesEmpty: 'इस सेशन में कोई फ़ाइल ऑपरेशन नहीं',
|
|
173
|
+
changesCount: '{count} फ़ाइल ऑपरेशन',
|
|
174
|
+
changesRedacted: '[मास्क किया गया]',
|
|
175
|
+
changesBinary: 'बाइनरी फ़ाइल — कोई पूर्वावलोकन नहीं',
|
|
176
|
+
changesPreviewError: 'पूर्वावलोकन पढ़ने में विफल: {message}',
|
|
177
|
+
changesLens: 'दृश्य',
|
|
170
178
|
exited: 'टर्मिनल प्रक्रिया बाहर निकली',
|
|
171
179
|
noSession: 'साइडबार उपयोग करने के लिए एक वार्तालाप चुनें',
|
|
172
180
|
pluginNotLoaded: 'प्लगइन लोड नहीं; टैब अनुपलब्ध:',
|
package/src/client/locales-id.ts
CHANGED
|
@@ -165,6 +165,14 @@ export const id: Record<string, string> = {
|
|
|
165
165
|
deleteDescFile: 'File ini dihapus permanen dan tidak dapat dibatalkan.',
|
|
166
166
|
deleteDescDir: 'Direktori ini dan seluruh isinya dihapus permanen dan tidak dapat dibatalkan.',
|
|
167
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',
|
|
168
176
|
exited: 'Proses terminal keluar',
|
|
169
177
|
noSession: 'Pilih obrolan untuk menggunakan sidebar',
|
|
170
178
|
pluginNotLoaded: 'Plugin tidak dimuat; tab tidak tersedia untuk sementara:',
|
package/src/client/locales-it.ts
CHANGED
|
@@ -158,6 +158,14 @@ export const it: Record<string, string> = {
|
|
|
158
158
|
deleteDescFile: 'Il file verrà eliminato definitivamente. Operazione irreversibile.',
|
|
159
159
|
deleteDescDir: 'La directory e tutto il suo contenuto verranno eliminati definitivamente. Operazione irreversibile.',
|
|
160
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',
|
|
161
169
|
exited: 'Il processo del terminale è terminato',
|
|
162
170
|
noSession: 'Selezioni una conversazione per usare la barra laterale',
|
|
163
171
|
pluginNotLoaded: 'Plugin non caricato; scheda non disponibile:',
|
package/src/client/locales-ja.ts
CHANGED
|
@@ -167,6 +167,14 @@ export const ja: Record<string, string> = {
|
|
|
167
167
|
deleteDescFile: 'このファイルは完全に削除されます。元に戻せません。',
|
|
168
168
|
deleteDescDir: 'このディレクトリとその内容は完全に削除されます。元に戻せません。',
|
|
169
169
|
dismiss: '閉じる',
|
|
170
|
+
changesSessionGit: 'Git 変更',
|
|
171
|
+
changesSessionLens: 'セッション変更',
|
|
172
|
+
changesEmpty: 'このセッションにはファイル操作がありません',
|
|
173
|
+
changesCount: '{count} 件のファイル操作',
|
|
174
|
+
changesRedacted: '[マスク済み]',
|
|
175
|
+
changesBinary: 'バイナリファイルのためプレビューできません',
|
|
176
|
+
changesPreviewError: 'プレビューの読み取りに失敗: {message}',
|
|
177
|
+
changesLens: 'ビュー',
|
|
170
178
|
exited: 'ターミナルプロセスが終了しました',
|
|
171
179
|
noSession: 'サイドバーを使うには会話を選択してください',
|
|
172
180
|
pluginNotLoaded: 'プラグイン未読み込み、タブは一時的に利用不可:',
|
package/src/client/locales-ko.ts
CHANGED
|
@@ -159,6 +159,14 @@ export const ko: Record<string, string> = {
|
|
|
159
159
|
deleteDescFile: '이 파일은 영구 삭제되며 되돌릴 수 없습니다.',
|
|
160
160
|
deleteDescDir: '이 디렉터리와 그 내용이 영구 삭제되며 되돌릴 수 없습니다.',
|
|
161
161
|
dismiss: '닫기',
|
|
162
|
+
changesSessionGit: 'Git 변경',
|
|
163
|
+
changesSessionLens: '세션 변경',
|
|
164
|
+
changesEmpty: '이 세션에는 파일 작업이 없습니다',
|
|
165
|
+
changesCount: '파일 작업 {count}건',
|
|
166
|
+
changesRedacted: '[마스킹됨]',
|
|
167
|
+
changesBinary: '바이너리 파일 — 미리보기 없음',
|
|
168
|
+
changesPreviewError: '미리보기 읽기 실패: {message}',
|
|
169
|
+
changesLens: '보기',
|
|
162
170
|
exited: '터미널 프로세스가 종료되었습니다',
|
|
163
171
|
noSession: '사이드바를 사용하려면 대화를 선택하세요',
|
|
164
172
|
pluginNotLoaded: '플러그인이 로드되지 않아 탭을 지금 사용할 수 없습니다:',
|
package/src/client/locales-nl.ts
CHANGED
|
@@ -165,6 +165,14 @@ export const nl: Record<string, string> = {
|
|
|
165
165
|
deleteDescFile: 'Dit verwijdert het bestand definitief. Dit kan niet ongedaan worden gemaakt.',
|
|
166
166
|
deleteDescDir: 'Dit verwijdert de map en de volledige inhoud definitief. Dit kan niet ongedaan worden gemaakt.',
|
|
167
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',
|
|
168
176
|
exited: 'Terminalproces beëindigd',
|
|
169
177
|
noSession: 'Selecteer een conversatie om de zijbalk te gebruiken',
|
|
170
178
|
pluginNotLoaded: 'Plugin niet geladen; tabblad niet beschikbaar:',
|
package/src/client/locales-pl.ts
CHANGED
|
@@ -169,6 +169,14 @@ export const pl: Record<string, string> = {
|
|
|
169
169
|
deleteDescFile: 'Ten plik zostanie trwale usunięty. Nie można tego cofnąć.',
|
|
170
170
|
deleteDescDir: 'Ten katalog i cała jego zawartość zostaną trwale usunięte. Nie można tego cofnąć.',
|
|
171
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',
|
|
172
180
|
exited: 'Proces terminala zakończony',
|
|
173
181
|
noSession: 'Wybierz rozmowę, aby korzystać z panelu bocznego',
|
|
174
182
|
pluginNotLoaded: 'Wtyczka niezaładowana; karta chwilowo niedostępna:',
|
package/src/client/locales-pt.ts
CHANGED
|
@@ -150,6 +150,14 @@ export const pt: Record<string, string> = {
|
|
|
150
150
|
deleteDescFile: 'Isto elimina definitivamente o ficheiro. Não pode ser anulado.',
|
|
151
151
|
deleteDescDir: 'Isto elimina definitivamente o diretório e todo o seu conteúdo. Não pode ser anulado.',
|
|
152
152
|
dismiss: 'Fechar',
|
|
153
|
+
changesSessionGit: 'Alterações do Git',
|
|
154
|
+
changesSessionLens: 'Alterações da sessão',
|
|
155
|
+
changesEmpty: 'Sem operações de ficheiro nesta sessão',
|
|
156
|
+
changesCount: '{count} operações de ficheiro',
|
|
157
|
+
changesRedacted: '[MASCARADO]',
|
|
158
|
+
changesBinary: 'Ficheiro binário — sem pré-visualização',
|
|
159
|
+
changesPreviewError: 'Falha ao ler a pré-visualização: {message}',
|
|
160
|
+
changesLens: 'Vista',
|
|
153
161
|
exited: 'O processo do terminal saiu',
|
|
154
162
|
noSession: 'Selecione uma conversa para usar a barra lateral',
|
|
155
163
|
pluginNotLoaded: 'Plugin não carregado; aba indisponível:',
|
package/src/client/locales-ru.ts
CHANGED
|
@@ -163,6 +163,14 @@ export const ru: Record<string, string> = {
|
|
|
163
163
|
deleteDescFile: 'Файл будет удалён безвозвратно. Действие необратимо.',
|
|
164
164
|
deleteDescDir: 'Каталог и всё его содержимое будут удалены безвозвратно. Действие необратимо.',
|
|
165
165
|
dismiss: 'Закрыть',
|
|
166
|
+
changesSessionGit: 'Изменения Git',
|
|
167
|
+
changesSessionLens: 'Изменения сессии',
|
|
168
|
+
changesEmpty: 'В этой сессии нет операций с файлами',
|
|
169
|
+
changesCount: 'Операций с файлами: {count}',
|
|
170
|
+
changesRedacted: '[СКРЫТО]',
|
|
171
|
+
changesBinary: 'Двоичный файл — предпросмотр недоступен',
|
|
172
|
+
changesPreviewError: 'Не удалось прочитать предпросмотр: {message}',
|
|
173
|
+
changesLens: 'Вид',
|
|
166
174
|
exited: 'Процесс терминала завершился',
|
|
167
175
|
noSession: 'Выберите сессию, чтобы использовать боковую панель',
|
|
168
176
|
pluginNotLoaded: 'Плагин не загружен — вкладка недоступна:',
|
package/src/client/locales-sv.ts
CHANGED
|
@@ -150,6 +150,14 @@ export const sv: Record<string, string> = {
|
|
|
150
150
|
deleteDescFile: 'Filen tas bort permanent. Detta kan inte ångras.',
|
|
151
151
|
deleteDescDir: 'Katalogen och allt i den tas bort permanent. Detta kan inte ångras.',
|
|
152
152
|
dismiss: 'Stäng',
|
|
153
|
+
changesSessionGit: 'Git-ändringar',
|
|
154
|
+
changesSessionLens: 'Sessionsändringar',
|
|
155
|
+
changesEmpty: 'Inga filåtgärder i den här sessionen',
|
|
156
|
+
changesCount: '{count} filåtgärder',
|
|
157
|
+
changesRedacted: '[SMETAT]',
|
|
158
|
+
changesBinary: 'Binärfil — ingen förhandsvisning',
|
|
159
|
+
changesPreviewError: 'Kunde inte läsa förhandsvisning: {message}',
|
|
160
|
+
changesLens: 'Vy',
|
|
153
161
|
exited: 'Terminalprocess avslutad',
|
|
154
162
|
noSession: 'Välj en konversation för att använda sidopanelen',
|
|
155
163
|
pluginNotLoaded: 'Plugin inte laddad; flik otillgänglig:',
|
package/src/client/locales-th.ts
CHANGED
|
@@ -167,6 +167,14 @@ export const th: Record<string, string> = {
|
|
|
167
167
|
deleteDescFile: 'ไฟล์นี้จะถูกลบอย่างถาวร ย้อนกลับไม่ได้',
|
|
168
168
|
deleteDescDir: 'ไดเรกทอรีนี้และเนื้อหาทั้งหมดจะถูกลบอย่างถาวร ย้อนกลับไม่ได้',
|
|
169
169
|
dismiss: 'ปิด',
|
|
170
|
+
changesSessionGit: 'การเปลี่ยนแปลง Git',
|
|
171
|
+
changesSessionLens: 'การเปลี่ยนแปลงในเซสชัน',
|
|
172
|
+
changesEmpty: 'ไม่มีการดำเนินการไฟล์ในเซสชันนี้',
|
|
173
|
+
changesCount: 'การดำเนินการไฟล์ {count} รายการ',
|
|
174
|
+
changesRedacted: '[ถูกปกปิด]',
|
|
175
|
+
changesBinary: 'ไฟล์ไบนารี — ไม่มีตัวอย่าง',
|
|
176
|
+
changesPreviewError: 'อ่านตัวอย่างไม่สำเร็จ: {message}',
|
|
177
|
+
changesLens: 'มุมมอง',
|
|
170
178
|
exited: 'กระบวนการเทอร์มินัลออกแล้ว',
|
|
171
179
|
noSession: 'เลือกแชทเพื่อใช้แถบด้านข้าง',
|
|
172
180
|
pluginNotLoaded: 'ปลั๊กอินไม่ได้โหลด; tab ไม่พร้อมใช้งาน:',
|