dsh-ide-git 0.13.0 → 0.13.2
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/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +236 -31
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-ide-git",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "IDE 级 Git 操作面板:分支树 + 提交图谱 + 变更/暂存/提交 + 提交详情。装了 dsh-better-sidebar 时挂成它的原生 tab(右侧栏 + 底部工作台);没装时直接挂 DSH 原生右侧栏,单独安装即可用。JetBrains 风格 Git 工具窗口 for DeepSeek Harness.",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ide-git",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "DSH plugin: an IDE-grade Git tool window as a native dsh-better-sidebar tab — branch tree, commit graph, changes, commit details, JetBrains-style actions, in the right sidebar and the bottom panel. | DSH 插件:IDE 级 Git 工具窗口,以 dsh-better-sidebar 原生 Tab 挂载——分支树 / 提交图谱 / 变更与提交详情 / JetBrains 风格操作,右侧栏与底部面板双布局自适应。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/client.js
CHANGED
|
@@ -27,6 +27,13 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
const useRef = React.useRef
|
|
28
28
|
const useCallback = React.useCallback
|
|
29
29
|
const useLayoutEffect = React.useLayoutEffect
|
|
30
|
+
const memo = React.memo
|
|
31
|
+
/* Measure-and-derive effects run here for the layout timing (the chrome and
|
|
32
|
+
the rail capacity derive from these numbers — a paint-then-correct cycle
|
|
33
|
+
read as buttons lighting up one by one on every seat switch). Environments
|
|
34
|
+
without a frame loop (the SSR self-check's shims) get the plain effect —
|
|
35
|
+
there is no paint to sync with there, and no server warning either. */
|
|
36
|
+
const useMeasureLayoutEffect = typeof requestAnimationFrame === 'function' ? useLayoutEffect : useEffect
|
|
30
37
|
|
|
31
38
|
/* API base is MOUNT-RELATIVE (no leading slash). dsh 0.1.7 serves the
|
|
32
39
|
shell with <base href="./">, so document.baseURI is the mount the page
|
|
@@ -4233,8 +4240,86 @@ window.__ModuleLoader__.load({
|
|
|
4233
4240
|
return body.data
|
|
4234
4241
|
}
|
|
4235
4242
|
|
|
4243
|
+
/* ============================== first-paint cache ============================== */
|
|
4244
|
+
|
|
4245
|
+
/* Session-scoped seats (the conversation view, and every sidebar seat that
|
|
4246
|
+
follows the active session) REMOUNT their panel on each session switch:
|
|
4247
|
+
the slot subtree belongs to the session. Re-running the whole discovery
|
|
4248
|
+
(repos scan) + first page (summary / branches / log — three git spawns)
|
|
4249
|
+
on every switch made the chrome re-appear piece by piece (reported
|
|
4250
|
+
against v0.13.0). Same workspace ⇒ same repository ⇒ the previous paint
|
|
4251
|
+
is still essentially correct, so it seeds the states and a background
|
|
4252
|
+
refresh lands the fresh data a moment later. Pure functions; the panel
|
|
4253
|
+
only ever reads them at effect time and writes after a fetch. */
|
|
4254
|
+
const PAINT_TTL_MS = 30_000
|
|
4255
|
+
const paintRepos = new Map() /* cwd -> { at, data } */
|
|
4256
|
+
const paintPage = new Map() /* repoRoot -> { at, summary, branches, commits, hasMore } */
|
|
4257
|
+
const paintRoot = new Map() /* cwd -> last repoRoot the user was looking at */
|
|
4258
|
+
|
|
4259
|
+
function readPaintRepos(cwd) {
|
|
4260
|
+
const entry = paintRepos.get(cwd)
|
|
4261
|
+
if (entry === undefined || Date.now() - entry.at > PAINT_TTL_MS) return undefined
|
|
4262
|
+
return entry.data
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
function writePaintRepos(cwd, data) {
|
|
4266
|
+
paintRepos.set(cwd, { at: Date.now(), data: data })
|
|
4267
|
+
if (paintRepos.size > 16) paintRepos.delete(paintRepos.keys().next().value)
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
function readPaintPage(repoRoot) {
|
|
4271
|
+
const entry = paintPage.get(repoRoot)
|
|
4272
|
+
if (entry === undefined || Date.now() - entry.at > PAINT_TTL_MS) return undefined
|
|
4273
|
+
return entry
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
function writePaintPage(repoRoot, page) {
|
|
4277
|
+
paintPage.set(repoRoot, Object.assign({ at: Date.now() }, page))
|
|
4278
|
+
if (paintPage.size > 16) paintPage.delete(paintPage.keys().next().value)
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
function rememberPaintRoot(cwd, repoRoot) {
|
|
4282
|
+
if (typeof cwd === 'string' && cwd !== '' && repoRoot !== null) paintRoot.set(cwd, repoRoot)
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
function readPaintRoot(cwd) {
|
|
4286
|
+
return paintRoot.get(cwd)
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4236
4289
|
/* ============================== storage ============================== */
|
|
4237
4290
|
|
|
4291
|
+
/* ---- tree UI state (v0.13.1): the session-scoped seats remount on every
|
|
4292
|
+
session switch, and component state died with them — branch sections
|
|
4293
|
+
snapped back to defaults and folders re-folded. One localStorage home,
|
|
4294
|
+
GLOBAL by design: which sections you keep folded is a habit, not a
|
|
4295
|
+
property of the repository. ---- */
|
|
4296
|
+
const TREE_KEY = 'dsh-ide-git.tree.v1'
|
|
4297
|
+
|
|
4298
|
+
function readTreeState() {
|
|
4299
|
+
try {
|
|
4300
|
+
const raw = window.localStorage.getItem(TREE_KEY)
|
|
4301
|
+
if (raw === null || raw === '') return null
|
|
4302
|
+
const parsed = JSON.parse(raw)
|
|
4303
|
+
return parsed !== null && typeof parsed === 'object' ? parsed : null
|
|
4304
|
+
} catch (error) { void error }
|
|
4305
|
+
return null
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
function writeTreeState(state) {
|
|
4309
|
+
try { window.localStorage.setItem(TREE_KEY, JSON.stringify(state)) } catch (error) { void error }
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
function treeSectionDefaults() {
|
|
4313
|
+
return { favorites: false, remote: false, tags: true }
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
function treeSectionsOf(stored) {
|
|
4317
|
+
const sections = stored !== null && typeof stored === 'object' && stored.sections !== null && typeof stored.sections === 'object'
|
|
4318
|
+
? stored.sections
|
|
4319
|
+
: {}
|
|
4320
|
+
return Object.assign(treeSectionDefaults(), sections)
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4238
4323
|
/* ---- ai commit pickers (issue #7): the model catalog and per-model
|
|
4239
4324
|
reasoning efforts, served by the host's commit-models / commit-efforts
|
|
4240
4325
|
routes. Cached for a short window so the settings card can mount twice
|
|
@@ -5975,9 +6060,23 @@ window.__ModuleLoader__.load({
|
|
|
5975
6060
|
function BranchTree(props) {
|
|
5976
6061
|
const t = props.t
|
|
5977
6062
|
const [filter, setFilter] = useState('')
|
|
5978
|
-
const [collapsed, setCollapsed] = useState(
|
|
6063
|
+
const [collapsed, setCollapsed] = useState(() => treeSectionsOf(readTreeState()))
|
|
5979
6064
|
const [picked, setPicked] = useState('')
|
|
5980
|
-
const [foldedFolders, setFoldedFolders] = useState({
|
|
6065
|
+
const [foldedFolders, setFoldedFolders] = useState(() => {
|
|
6066
|
+
const stored = readTreeState()
|
|
6067
|
+
return stored !== null && typeof stored === 'object' && stored.folders !== null && typeof stored.folders === 'object'
|
|
6068
|
+
? stored.folders
|
|
6069
|
+
: {}
|
|
6070
|
+
})
|
|
6071
|
+
/* Survive the remount a session switch performs (v0.13.1). */
|
|
6072
|
+
useEffect(() => {
|
|
6073
|
+
const stored = readTreeState()
|
|
6074
|
+
writeTreeState(Object.assign({}, stored, { sections: collapsed }))
|
|
6075
|
+
}, [collapsed])
|
|
6076
|
+
useEffect(() => {
|
|
6077
|
+
const stored = readTreeState()
|
|
6078
|
+
writeTreeState(Object.assign({}, stored, { folders: foldedFolders }))
|
|
6079
|
+
}, [foldedFolders])
|
|
5981
6080
|
const branches = props.branches
|
|
5982
6081
|
const needle = filter.trim().toLowerCase()
|
|
5983
6082
|
const match = (name) => needle === '' || String(name).toLowerCase().indexOf(needle) >= 0
|
|
@@ -6093,7 +6192,15 @@ window.__ModuleLoader__.load({
|
|
|
6093
6192
|
const summary = props.summary
|
|
6094
6193
|
const [message, setMessage] = useState('')
|
|
6095
6194
|
const [amend, setAmend] = useState(false)
|
|
6096
|
-
const [collapsed, setCollapsed] = useState(
|
|
6195
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
6196
|
+
const stored = readTreeState()
|
|
6197
|
+
return stored !== null && typeof stored === 'object' && stored.changesCollapsed === true
|
|
6198
|
+
})
|
|
6199
|
+
/* Same remount-survival as the branch sections (v0.13.1). */
|
|
6200
|
+
useEffect(() => {
|
|
6201
|
+
const stored = readTreeState()
|
|
6202
|
+
writeTreeState(Object.assign({}, stored, { changesCollapsed: collapsed === true }))
|
|
6203
|
+
}, [collapsed])
|
|
6097
6204
|
/* AI-written message (issue #6). The draft is kept so the write can be
|
|
6098
6205
|
undone, and an existing draft is never overwritten without a word. */
|
|
6099
6206
|
const [aiBusy, setAiBusy] = useState(false)
|
|
@@ -6573,7 +6680,12 @@ window.__ModuleLoader__.load({
|
|
|
6573
6680
|
const sessionId = typeof scope.sessionId === 'string' ? scope.sessionId : 'default'
|
|
6574
6681
|
const base = useMemo(() => ({ cwd: cwd, repoRoot: repoRoot === null ? undefined : repoRoot }), [cwd, repoRoot])
|
|
6575
6682
|
|
|
6576
|
-
|
|
6683
|
+
/* Layout effect ON PURPOSE: the chrome (compact/columns/stack) and the
|
|
6684
|
+
rail capacity derive from these numbers, and a paint-then-correct
|
|
6685
|
+
cycle showed up on every seat switch as buttons lighting up one by
|
|
6686
|
+
one (v0.13.1 report). Measuring in the commit phase keeps the first
|
|
6687
|
+
paint on the final chrome. */
|
|
6688
|
+
useMeasureLayoutEffect(() => {
|
|
6577
6689
|
const element = hostRef.current
|
|
6578
6690
|
if (element === null) return undefined
|
|
6579
6691
|
const measure = () => {
|
|
@@ -6617,7 +6729,7 @@ window.__ModuleLoader__.load({
|
|
|
6617
6729
|
// Like the rail, the panes measure the box that actually holds them instead
|
|
6618
6730
|
// of trusting the root probe: the column chrome keeps a vertical rail
|
|
6619
6731
|
// beside the body, and the header sits above it.
|
|
6620
|
-
|
|
6732
|
+
useMeasureLayoutEffect(() => {
|
|
6621
6733
|
const element = bodyRef.current
|
|
6622
6734
|
if (element === null) { setBodyBox({ width: 0, height: 0 }); return undefined }
|
|
6623
6735
|
const measure = () => {
|
|
@@ -6755,7 +6867,9 @@ window.__ModuleLoader__.load({
|
|
|
6755
6867
|
// function of the strip that holds them (a 1200x300 workbench and a 300x900
|
|
6756
6868
|
// sidebar disagree about that). railBox keeps the last measured span; until
|
|
6757
6869
|
// the first measurement lands, every action is rendered.
|
|
6758
|
-
|
|
6870
|
+
// Layout effect (see the host measure above): the button count derives
|
|
6871
|
+
// from this box, and a late measure reads as the rail lighting up late.
|
|
6872
|
+
useMeasureLayoutEffect(() => {
|
|
6759
6873
|
const element = railHostRef.current
|
|
6760
6874
|
if (element === null) { setRailBox({ width: 0, height: 0 }); return undefined }
|
|
6761
6875
|
const measure = () => {
|
|
@@ -6798,18 +6912,30 @@ window.__ModuleLoader__.load({
|
|
|
6798
6912
|
as "still loading" and would spin forever). */
|
|
6799
6913
|
if (cwd === undefined) { setRepoState({ repos: [], isRepo: false }); return undefined }
|
|
6800
6914
|
let cancelled = false
|
|
6915
|
+
/* First-paint cache: a session switch through the same workspace
|
|
6916
|
+
reuses the previous discovery synchronously (repo bar and pick land
|
|
6917
|
+
with the first paint), then the background scan re-proves it. */
|
|
6918
|
+
const apply = (data, cachedPick) => {
|
|
6919
|
+
setRepoState(data)
|
|
6920
|
+
const paths = data.repos.map((repo) => repo.path)
|
|
6921
|
+
const remembered = readRememberedRepo(sessionId)
|
|
6922
|
+
let pick = null
|
|
6923
|
+
if (remembered !== undefined && paths.indexOf(remembered) >= 0) pick = remembered
|
|
6924
|
+
else if (cachedPick !== undefined && paths.indexOf(cachedPick) >= 0) pick = cachedPick
|
|
6925
|
+
else if (data.isRepo === true) pick = data.cwd
|
|
6926
|
+
else if (paths.length > 0) pick = paths[0]
|
|
6927
|
+
setRepoRoot(pick)
|
|
6928
|
+
rememberPaintRoot(cwd, pick)
|
|
6929
|
+
return pick
|
|
6930
|
+
}
|
|
6931
|
+
const cached = readPaintRepos(cwd)
|
|
6932
|
+
if (cached !== undefined) apply(cached, readPaintRoot(cwd))
|
|
6801
6933
|
void (async () => {
|
|
6802
6934
|
try {
|
|
6803
6935
|
const data = await request('repos', { cwd: cwd })
|
|
6804
6936
|
if (cancelled) return
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
const remembered = readRememberedRepo(sessionId)
|
|
6808
|
-
let pick = null
|
|
6809
|
-
if (remembered !== undefined && paths.indexOf(remembered) >= 0) pick = remembered
|
|
6810
|
-
else if (data.isRepo === true) pick = data.cwd
|
|
6811
|
-
else if (paths.length > 0) pick = paths[0]
|
|
6812
|
-
setRepoRoot(pick)
|
|
6937
|
+
writePaintRepos(cwd, data)
|
|
6938
|
+
apply(data, readPaintRoot(cwd))
|
|
6813
6939
|
} catch (caught) {
|
|
6814
6940
|
if (!cancelled) setError(caught instanceof Error ? caught.message : String(caught))
|
|
6815
6941
|
}
|
|
@@ -6824,7 +6950,9 @@ window.__ModuleLoader__.load({
|
|
|
6824
6950
|
}, [base, showIgnored])
|
|
6825
6951
|
|
|
6826
6952
|
const loadBranches = useCallback(async () => {
|
|
6827
|
-
|
|
6953
|
+
const data = await request('branches', base)
|
|
6954
|
+
setBranches(data)
|
|
6955
|
+
return data
|
|
6828
6956
|
}, [base])
|
|
6829
6957
|
|
|
6830
6958
|
const loadCommits = useCallback(async (skip, overridePath) => {
|
|
@@ -6834,19 +6962,39 @@ window.__ModuleLoader__.load({
|
|
|
6834
6962
|
const data = await request('log', payload)
|
|
6835
6963
|
setCommits((previous) => (skip === undefined || skip === 0 ? data.commits : previous.concat(data.commits)))
|
|
6836
6964
|
setHasMore(data.hasMore)
|
|
6965
|
+
return data
|
|
6837
6966
|
}, [base, pathFilter])
|
|
6838
6967
|
|
|
6839
6968
|
const refresh = useCallback(async () => {
|
|
6840
6969
|
if (repoRoot === null) return
|
|
6841
6970
|
await guard(async () => {
|
|
6842
|
-
await Promise.all([loadSummary(), loadBranches(), loadCommits(0)])
|
|
6971
|
+
const [summaryData, branchesData, page] = await Promise.all([loadSummary(), loadBranches(), loadCommits(0)])
|
|
6972
|
+
/* The write side of the first-paint cache: exactly the FIRST page
|
|
6973
|
+
(a paged fetch never reaches this path), replaced on every
|
|
6974
|
+
refresh — the auto-refresh included — so a session switch through
|
|
6975
|
+
the same workspace seeds fresh data, not the mount-time
|
|
6976
|
+
snapshot. */
|
|
6977
|
+
writePaintPage(repoRoot, { summary: summaryData, branches: branchesData, commits: page.commits, hasMore: page.hasMore })
|
|
6843
6978
|
})
|
|
6844
6979
|
}, [guard, loadSummary, loadBranches, loadCommits, repoRoot])
|
|
6845
6980
|
|
|
6846
6981
|
useEffect(() => {
|
|
6847
6982
|
if (repoRoot === null) return undefined
|
|
6983
|
+
/* First-paint cache: seed the first page synchronously so a session
|
|
6984
|
+
switch through the same workspace paints the full chrome (rail,
|
|
6985
|
+
branch bar, history) at once instead of re-growing it piece by
|
|
6986
|
+
piece; the refresh below re-proves every field right after. Only a
|
|
6987
|
+
cold mount seeds — a later effect run (tick / repo switch) must not
|
|
6988
|
+
roll live state back to a stale snapshot. */
|
|
6989
|
+
const cachedPage = readPaintPage(repoRoot)
|
|
6990
|
+
if (cachedPage !== undefined) {
|
|
6991
|
+
if (summary === null) setSummary(cachedPage.summary)
|
|
6992
|
+
if (branches === null) setBranches(cachedPage.branches)
|
|
6993
|
+
if (commits.length === 0) { setCommits(cachedPage.commits); setHasMore(cachedPage.hasMore) }
|
|
6994
|
+
}
|
|
6848
6995
|
void refresh()
|
|
6849
6996
|
return undefined
|
|
6997
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
6850
6998
|
}, [repoRoot, refresh, tick])
|
|
6851
6999
|
|
|
6852
7000
|
useEffect(() => {
|
|
@@ -7636,7 +7784,7 @@ window.__ModuleLoader__.load({
|
|
|
7636
7784
|
|
|
7637
7785
|
const CSS = [
|
|
7638
7786
|
'.dig-root{position:relative;display:flex;flex-direction:column;height:100%;min-height:0;background:transparent;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,system-ui,sans-serif);font-size:13px;line-height:1.5;font-weight:600;overflow:hidden}',
|
|
7639
|
-
'.dig-topbar{display:flex;align-items:center;gap:6px;padding:4px 6px;flex:none;min-width:0;border-bottom:1px solid var(--dsw-alias-
|
|
7787
|
+
'.dig-topbar{display:flex;align-items:center;gap:6px;padding:4px 6px;flex:none;min-width:0;border-bottom:1px solid var(--dsw-alias-border-l2);overflow:hidden}',
|
|
7640
7788
|
'.dig-topbar-spacer{flex:1;min-width:4px}',
|
|
7641
7789
|
'.dig-busy{color:var(--dsw-alias-label-tertiary);flex:none;white-space:nowrap}',
|
|
7642
7790
|
'.dig-track{color:var(--dsw-alias-label-secondary);flex:none;font-variant-numeric:tabular-nums;white-space:nowrap}',
|
|
@@ -7659,10 +7807,10 @@ window.__ModuleLoader__.load({
|
|
|
7659
7807
|
'.dig-note{display:flex;align-items:center;gap:8px;padding:4px 8px;flex:none;color:var(--dsw-alias-label-secondary)}',
|
|
7660
7808
|
'.dig-banner-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}',
|
|
7661
7809
|
'.dig-shell{flex:1;min-height:0;display:flex;overflow:hidden}',
|
|
7662
|
-
'.dig-rail{width:32px;flex:none;display:flex;flex-direction:column;align-items:center;gap:2px;padding:4px 0;overflow:hidden;border-right:1px solid var(--dsw-alias-
|
|
7810
|
+
'.dig-rail{width:32px;flex:none;display:flex;flex-direction:column;align-items:center;gap:2px;padding:4px 0;overflow:hidden;border-right:1px solid var(--dsw-alias-border-l2)}',
|
|
7663
7811
|
'.dig-rail-gap{flex:1;min-height:2px}',
|
|
7664
7812
|
'.dig-rail-gap-x{flex:1;min-width:2px}',
|
|
7665
|
-
'.dig-rail-row{display:flex;align-items:center;gap:2px;padding:3px 6px;flex:none;overflow:hidden;border-bottom:1px solid var(--dsw-alias-
|
|
7813
|
+
'.dig-rail-row{display:flex;align-items:center;gap:2px;padding:3px 6px;flex:none;overflow:hidden;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
7666
7814
|
'.dig-rail-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;border-radius:var(--dsw-radius-sm,8px);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0;flex:none}',
|
|
7667
7815
|
'.dig-rail-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
7668
7816
|
'.dig-rail-btn:disabled{opacity:.28;cursor:default}',
|
|
@@ -7745,13 +7893,13 @@ window.__ModuleLoader__.load({
|
|
|
7745
7893
|
'.dig-body-stack{flex-direction:column}',
|
|
7746
7894
|
'.dig-body-compact{flex-direction:column}',
|
|
7747
7895
|
'.dig-pane{display:flex;flex-direction:column;min-height:0;min-width:0}',
|
|
7748
|
-
'.dig-pane-tree{width:200px;flex:none;border-right:1px solid var(--dsw-alias-
|
|
7749
|
-
'.dig-pane-tree-stack{max-height:36%;flex:none;border-bottom:1px solid var(--dsw-alias-
|
|
7896
|
+
'.dig-pane-tree{width:200px;flex:none;border-right:1px solid var(--dsw-alias-border-l2)}',
|
|
7897
|
+
'.dig-pane-tree-stack{max-height:36%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
7750
7898
|
'.dig-pane-main{flex:1;min-width:0}',
|
|
7751
|
-
'.dig-pane-changes{width:290px;flex:none;border-left:1px solid var(--dsw-alias-
|
|
7752
|
-
'.dig-pane-changes-stack{max-height:46%;flex:none;border-bottom:1px solid var(--dsw-alias-
|
|
7899
|
+
'.dig-pane-changes{width:290px;flex:none;border-left:1px solid var(--dsw-alias-border-l2)}',
|
|
7900
|
+
'.dig-pane-changes-stack{max-height:46%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
7753
7901
|
'.dig-compact-bar{display:flex;align-items:center;gap:6px;padding:4px 6px;flex:none}',
|
|
7754
|
-
'.dig-compact-tree{max-height:42%;flex:none;border-bottom:1px solid var(--dsw-alias-
|
|
7902
|
+
'.dig-compact-tree{max-height:42%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
7755
7903
|
// A divider: an 8px hit area (a fingertip needs it) painted as a 1px hairline
|
|
7756
7904
|
// that lights up on hover, focus or while dragging. touch-action:none is what
|
|
7757
7905
|
// makes a touch drag resize the pane instead of scrolling the panel.
|
|
@@ -7835,7 +7983,7 @@ window.__ModuleLoader__.load({
|
|
|
7835
7983
|
'.dig-ref-remote{color:#b083f0}',
|
|
7836
7984
|
'.dig-ref-tag{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary))}',
|
|
7837
7985
|
'.dig-ref-more{color:var(--dsw-alias-label-tertiary)}',
|
|
7838
|
-
'.dig-filters{display:flex;align-items:center;gap:4px;padding:4px 6px;flex:none;min-width:0;overflow-x:auto;overflow-y:hidden;border-bottom:1px solid var(--dsw-alias-
|
|
7986
|
+
'.dig-filters{display:flex;align-items:center;gap:4px;padding:4px 6px;flex:none;min-width:0;overflow-x:auto;overflow-y:hidden;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
7839
7987
|
'.dig-filter-text{flex:1 1 90px;min-width:80px;width:auto}',
|
|
7840
7988
|
'.dig-filter-select{flex:none;max-width:118px;appearance:none;-webkit-appearance:none;background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,8px);color:var(--dsw-alias-label-secondary);font:inherit;font-weight:500;height:22px;padding:0 4px;cursor:pointer;text-overflow:ellipsis}',
|
|
7841
7989
|
'.dig-filter-select:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
@@ -7865,7 +8013,7 @@ window.__ModuleLoader__.load({
|
|
|
7865
8013
|
'.dig-mini{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border:none;border-radius:var(--dsw-radius-xs,4px);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;flex:none;padding:0}',
|
|
7866
8014
|
'.dig-row:hover .dig-mini{opacity:1}',
|
|
7867
8015
|
'.dig-mini:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
7868
|
-
'.dig-commit-box{flex:none;padding:6px;display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--dsw-alias-
|
|
8016
|
+
'.dig-commit-box{flex:none;padding:6px;display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--dsw-alias-border-l2)}',
|
|
7869
8017
|
'.dig-commit-box-compact{flex-direction:row;align-items:center;gap:6px;padding:4px 6px;flex-wrap:wrap}',
|
|
7870
8018
|
'.dig-textarea{width:100%;box-sizing:border-box;min-height:50px;resize:vertical;padding:5px 6px;border-radius:var(--dsw-radius-sm,8px);border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;outline:none}',
|
|
7871
8019
|
'.dig-textarea::placeholder{color:var(--dsw-alias-label-tertiary)}',
|
|
@@ -7887,7 +8035,7 @@ window.__ModuleLoader__.load({
|
|
|
7887
8035
|
'.dig-detail-files-head{font-weight:600;margin-top:4px}',
|
|
7888
8036
|
'.dig-detail-files{display:flex;flex-direction:column}',
|
|
7889
8037
|
'.dig-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
|
|
7890
|
-
'.dig-diff-pane{flex:none;max-height:55%;display:flex;flex-direction:column;min-height:0;border-top:1px solid var(--dsw-alias-
|
|
8038
|
+
'.dig-diff-pane{flex:none;max-height:55%;display:flex;flex-direction:column;min-height:0;border-top:1px solid var(--dsw-alias-border-l2)}',
|
|
7891
8039
|
'.dig-diff-head{display:flex;align-items:center;gap:8px;padding:3px 8px;color:var(--dsw-alias-label-tertiary);flex:none;min-width:0}',
|
|
7892
8040
|
'.dig-diff-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}',
|
|
7893
8041
|
'.dig-diff{flex:1;overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.5;font-weight:500;min-height:0}',
|
|
@@ -7984,17 +8132,70 @@ window.__ModuleLoader__.load({
|
|
|
7984
8132
|
return undefined
|
|
7985
8133
|
}
|
|
7986
8134
|
|
|
7987
|
-
|
|
8135
|
+
/* The conversation shell re-renders on EVERY keystroke (the composer owns
|
|
8136
|
+
the draft state), and the view area re-renders with it. Without a bail-out
|
|
8137
|
+
at this boundary that cascade dragged the ENTIRE Git panel tree — branch
|
|
8138
|
+
rows, graph lanes, changes — through a synchronous render per keypress
|
|
8139
|
+
while the Git view was the active one (typing lag, reported against
|
|
8140
|
+
v0.13.0). The component consumes exactly three stable props (sessionId,
|
|
8141
|
+
t, ctx); everything else the slot passes (inspectCall, viewRequest, …)
|
|
8142
|
+
changes identity per shell render and is deliberately ignored, so the
|
|
8143
|
+
comparator is: same session, same bindings → skip the subtree. cwd moves
|
|
8144
|
+
through the sessions subscription inside, not through props. */
|
|
8145
|
+
const ConversationPanel = memo(function ConversationPanel(props) {
|
|
7988
8146
|
const ctx = props.ctx
|
|
7989
8147
|
const sessionId = typeof props.sessionId === 'string' ? props.sessionId : ''
|
|
7990
8148
|
const [cwd, setCwd] = useState(() => sessionCwdOf(ctx, sessionId))
|
|
8149
|
+
/* The active conversation's view area is `flex: 1 0 auto; min-height:
|
|
8150
|
+
auto` — it GROWS with content and scrolls inside the shared
|
|
8151
|
+
conversation scrollport. An auto-height panel there measured
|
|
8152
|
+
content-tall (a 120-commit history, a long diff), so the width-vs-
|
|
8153
|
+
height chrome test flipped the wide seat into the stacked sidebar
|
|
8154
|
+
layout and everything shifted downward (v0.13.1 report). Pin the seat
|
|
8155
|
+
to the scrollport's VISIBLE height instead: the panel fills exactly
|
|
8156
|
+
the on-screen area and scrolls internally, the way the sidebar seats
|
|
8157
|
+
behave. */
|
|
8158
|
+
const seatRef = useRef(null)
|
|
8159
|
+
const [seatHeight, setSeatHeight] = useState(null)
|
|
8160
|
+
useEffect(() => {
|
|
8161
|
+
const el = seatRef.current
|
|
8162
|
+
if (el === null) return undefined
|
|
8163
|
+
const measure = () => {
|
|
8164
|
+
let scroll = null
|
|
8165
|
+
try { scroll = el.closest('[data-conversation-scroll]') } catch (error) { void error }
|
|
8166
|
+
if (scroll === null || scroll === undefined) return
|
|
8167
|
+
const offset = Math.max(0, el.getBoundingClientRect().top - scroll.getBoundingClientRect().top)
|
|
8168
|
+
const available = Math.round(scroll.clientHeight - offset - 8)
|
|
8169
|
+
if (available >= 240) setSeatHeight(available)
|
|
8170
|
+
}
|
|
8171
|
+
measure()
|
|
8172
|
+
const raf = requestAnimationFrame(() => { measure() })
|
|
8173
|
+
const late = setTimeout(() => { measure() }, 400)
|
|
8174
|
+
let observer
|
|
8175
|
+
try {
|
|
8176
|
+
const scroll = el.closest('[data-conversation-scroll]')
|
|
8177
|
+
if (scroll !== null && scroll !== undefined && typeof ResizeObserver === 'function') {
|
|
8178
|
+
observer = new ResizeObserver(() => { measure() })
|
|
8179
|
+
observer.observe(scroll)
|
|
8180
|
+
}
|
|
8181
|
+
} catch (error) { void error }
|
|
8182
|
+
window.addEventListener('resize', measure)
|
|
8183
|
+
return () => {
|
|
8184
|
+
cancelAnimationFrame(raf)
|
|
8185
|
+
clearTimeout(late)
|
|
8186
|
+
if (observer !== undefined) observer.disconnect()
|
|
8187
|
+
window.removeEventListener('resize', measure)
|
|
8188
|
+
}
|
|
8189
|
+
}, [])
|
|
7991
8190
|
useEffect(() => {
|
|
7992
8191
|
let alive = true
|
|
7993
8192
|
const sync = () => { if (alive === true) setCwd(sessionCwdOf(ctx, sessionId)) }
|
|
7994
8193
|
sync()
|
|
7995
8194
|
/* The session list is a live store; without a subscription a later
|
|
7996
8195
|
start (first message in a blank session) would leave the tab on the
|
|
7997
|
-
no-repo chrome until the next remount.
|
|
8196
|
+
no-repo chrome until the next remount. The notify walks are cheap:
|
|
8197
|
+
getSnapshot + a string compare, and setCwd with an identical string
|
|
8198
|
+
bails out without a render. */
|
|
7998
8199
|
let unsubscribe
|
|
7999
8200
|
try {
|
|
8000
8201
|
const sessions = ctx.get('sessions')
|
|
@@ -8009,8 +8210,12 @@ window.__ModuleLoader__.load({
|
|
|
8009
8210
|
}
|
|
8010
8211
|
}, [ctx, sessionId])
|
|
8011
8212
|
const scope = useMemo(() => ({ cwd: cwd, sessionId: sessionId }), [cwd, sessionId])
|
|
8012
|
-
return E(
|
|
8013
|
-
|
|
8213
|
+
return E('div', {
|
|
8214
|
+
ref: seatRef,
|
|
8215
|
+
className: 'dig-conversation-seat',
|
|
8216
|
+
style: seatHeight === null ? undefined : { height: seatHeight + 'px' },
|
|
8217
|
+
}, E(LocaleLive, { ctx: ctx, scope: scope, t: props.t, visible: true }))
|
|
8218
|
+
}, (prev, next) => prev.sessionId === next.sessionId && prev.t === next.t && prev.ctx === next.ctx)
|
|
8014
8219
|
|
|
8015
8220
|
function NativePanel(props) {
|
|
8016
8221
|
const workspaces = typeof props.useWorkspaces === 'function'
|