dsh-ide-git 0.13.0 → 0.13.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-ide-git",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
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.0",
3
+ "version": "0.13.1",
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,7 @@ 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
30
31
 
31
32
  /* API base is MOUNT-RELATIVE (no leading slash). dsh 0.1.7 serves the
32
33
  shell with <base href="./">, so document.baseURI is the mount the page
@@ -4233,6 +4234,52 @@ window.__ModuleLoader__.load({
4233
4234
  return body.data
4234
4235
  }
4235
4236
 
4237
+ /* ============================== first-paint cache ============================== */
4238
+
4239
+ /* Session-scoped seats (the conversation view, and every sidebar seat that
4240
+ follows the active session) REMOUNT their panel on each session switch:
4241
+ the slot subtree belongs to the session. Re-running the whole discovery
4242
+ (repos scan) + first page (summary / branches / log — three git spawns)
4243
+ on every switch made the chrome re-appear piece by piece (reported
4244
+ against v0.13.0). Same workspace ⇒ same repository ⇒ the previous paint
4245
+ is still essentially correct, so it seeds the states and a background
4246
+ refresh lands the fresh data a moment later. Pure functions; the panel
4247
+ only ever reads them at effect time and writes after a fetch. */
4248
+ const PAINT_TTL_MS = 30_000
4249
+ const paintRepos = new Map() /* cwd -> { at, data } */
4250
+ const paintPage = new Map() /* repoRoot -> { at, summary, branches, commits, hasMore } */
4251
+ const paintRoot = new Map() /* cwd -> last repoRoot the user was looking at */
4252
+
4253
+ function readPaintRepos(cwd) {
4254
+ const entry = paintRepos.get(cwd)
4255
+ if (entry === undefined || Date.now() - entry.at > PAINT_TTL_MS) return undefined
4256
+ return entry.data
4257
+ }
4258
+
4259
+ function writePaintRepos(cwd, data) {
4260
+ paintRepos.set(cwd, { at: Date.now(), data: data })
4261
+ if (paintRepos.size > 16) paintRepos.delete(paintRepos.keys().next().value)
4262
+ }
4263
+
4264
+ function readPaintPage(repoRoot) {
4265
+ const entry = paintPage.get(repoRoot)
4266
+ if (entry === undefined || Date.now() - entry.at > PAINT_TTL_MS) return undefined
4267
+ return entry
4268
+ }
4269
+
4270
+ function writePaintPage(repoRoot, page) {
4271
+ paintPage.set(repoRoot, Object.assign({ at: Date.now() }, page))
4272
+ if (paintPage.size > 16) paintPage.delete(paintPage.keys().next().value)
4273
+ }
4274
+
4275
+ function rememberPaintRoot(cwd, repoRoot) {
4276
+ if (typeof cwd === 'string' && cwd !== '' && repoRoot !== null) paintRoot.set(cwd, repoRoot)
4277
+ }
4278
+
4279
+ function readPaintRoot(cwd) {
4280
+ return paintRoot.get(cwd)
4281
+ }
4282
+
4236
4283
  /* ============================== storage ============================== */
4237
4284
 
4238
4285
  /* ---- ai commit pickers (issue #7): the model catalog and per-model
@@ -6798,18 +6845,30 @@ window.__ModuleLoader__.load({
6798
6845
  as "still loading" and would spin forever). */
6799
6846
  if (cwd === undefined) { setRepoState({ repos: [], isRepo: false }); return undefined }
6800
6847
  let cancelled = false
6848
+ /* First-paint cache: a session switch through the same workspace
6849
+ reuses the previous discovery synchronously (repo bar and pick land
6850
+ with the first paint), then the background scan re-proves it. */
6851
+ const apply = (data, cachedPick) => {
6852
+ setRepoState(data)
6853
+ const paths = data.repos.map((repo) => repo.path)
6854
+ const remembered = readRememberedRepo(sessionId)
6855
+ let pick = null
6856
+ if (remembered !== undefined && paths.indexOf(remembered) >= 0) pick = remembered
6857
+ else if (cachedPick !== undefined && paths.indexOf(cachedPick) >= 0) pick = cachedPick
6858
+ else if (data.isRepo === true) pick = data.cwd
6859
+ else if (paths.length > 0) pick = paths[0]
6860
+ setRepoRoot(pick)
6861
+ rememberPaintRoot(cwd, pick)
6862
+ return pick
6863
+ }
6864
+ const cached = readPaintRepos(cwd)
6865
+ if (cached !== undefined) apply(cached, readPaintRoot(cwd))
6801
6866
  void (async () => {
6802
6867
  try {
6803
6868
  const data = await request('repos', { cwd: cwd })
6804
6869
  if (cancelled) return
6805
- setRepoState(data)
6806
- const paths = data.repos.map((repo) => repo.path)
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)
6870
+ writePaintRepos(cwd, data)
6871
+ apply(data, readPaintRoot(cwd))
6813
6872
  } catch (caught) {
6814
6873
  if (!cancelled) setError(caught instanceof Error ? caught.message : String(caught))
6815
6874
  }
@@ -6824,7 +6883,9 @@ window.__ModuleLoader__.load({
6824
6883
  }, [base, showIgnored])
6825
6884
 
6826
6885
  const loadBranches = useCallback(async () => {
6827
- setBranches(await request('branches', base))
6886
+ const data = await request('branches', base)
6887
+ setBranches(data)
6888
+ return data
6828
6889
  }, [base])
6829
6890
 
6830
6891
  const loadCommits = useCallback(async (skip, overridePath) => {
@@ -6834,19 +6895,39 @@ window.__ModuleLoader__.load({
6834
6895
  const data = await request('log', payload)
6835
6896
  setCommits((previous) => (skip === undefined || skip === 0 ? data.commits : previous.concat(data.commits)))
6836
6897
  setHasMore(data.hasMore)
6898
+ return data
6837
6899
  }, [base, pathFilter])
6838
6900
 
6839
6901
  const refresh = useCallback(async () => {
6840
6902
  if (repoRoot === null) return
6841
6903
  await guard(async () => {
6842
- await Promise.all([loadSummary(), loadBranches(), loadCommits(0)])
6904
+ const [summaryData, branchesData, page] = await Promise.all([loadSummary(), loadBranches(), loadCommits(0)])
6905
+ /* The write side of the first-paint cache: exactly the FIRST page
6906
+ (a paged fetch never reaches this path), replaced on every
6907
+ refresh — the auto-refresh included — so a session switch through
6908
+ the same workspace seeds fresh data, not the mount-time
6909
+ snapshot. */
6910
+ writePaintPage(repoRoot, { summary: summaryData, branches: branchesData, commits: page.commits, hasMore: page.hasMore })
6843
6911
  })
6844
6912
  }, [guard, loadSummary, loadBranches, loadCommits, repoRoot])
6845
6913
 
6846
6914
  useEffect(() => {
6847
6915
  if (repoRoot === null) return undefined
6916
+ /* First-paint cache: seed the first page synchronously so a session
6917
+ switch through the same workspace paints the full chrome (rail,
6918
+ branch bar, history) at once instead of re-growing it piece by
6919
+ piece; the refresh below re-proves every field right after. Only a
6920
+ cold mount seeds — a later effect run (tick / repo switch) must not
6921
+ roll live state back to a stale snapshot. */
6922
+ const cachedPage = readPaintPage(repoRoot)
6923
+ if (cachedPage !== undefined) {
6924
+ if (summary === null) setSummary(cachedPage.summary)
6925
+ if (branches === null) setBranches(cachedPage.branches)
6926
+ if (commits.length === 0) { setCommits(cachedPage.commits); setHasMore(cachedPage.hasMore) }
6927
+ }
6848
6928
  void refresh()
6849
6929
  return undefined
6930
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6850
6931
  }, [repoRoot, refresh, tick])
6851
6932
 
6852
6933
  useEffect(() => {
@@ -7636,7 +7717,7 @@ window.__ModuleLoader__.load({
7636
7717
 
7637
7718
  const CSS = [
7638
7719
  '.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-hairline,var(--dsw-alias-border-l1));overflow:hidden}',
7720
+ '.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
7721
  '.dig-topbar-spacer{flex:1;min-width:4px}',
7641
7722
  '.dig-busy{color:var(--dsw-alias-label-tertiary);flex:none;white-space:nowrap}',
7642
7723
  '.dig-track{color:var(--dsw-alias-label-secondary);flex:none;font-variant-numeric:tabular-nums;white-space:nowrap}',
@@ -7659,10 +7740,10 @@ window.__ModuleLoader__.load({
7659
7740
  '.dig-note{display:flex;align-items:center;gap:8px;padding:4px 8px;flex:none;color:var(--dsw-alias-label-secondary)}',
7660
7741
  '.dig-banner-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}',
7661
7742
  '.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-hairline,var(--dsw-alias-border-l1))}',
7743
+ '.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
7744
  '.dig-rail-gap{flex:1;min-height:2px}',
7664
7745
  '.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-hairline,var(--dsw-alias-border-l1))}',
7746
+ '.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
7747
  '.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
7748
  '.dig-rail-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
7668
7749
  '.dig-rail-btn:disabled{opacity:.28;cursor:default}',
@@ -7745,13 +7826,13 @@ window.__ModuleLoader__.load({
7745
7826
  '.dig-body-stack{flex-direction:column}',
7746
7827
  '.dig-body-compact{flex-direction:column}',
7747
7828
  '.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-hairline,var(--dsw-alias-border-l1))}',
7749
- '.dig-pane-tree-stack{max-height:36%;flex:none;border-bottom:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7829
+ '.dig-pane-tree{width:200px;flex:none;border-right:1px solid var(--dsw-alias-border-l2)}',
7830
+ '.dig-pane-tree-stack{max-height:36%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
7750
7831
  '.dig-pane-main{flex:1;min-width:0}',
7751
- '.dig-pane-changes{width:290px;flex:none;border-left:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7752
- '.dig-pane-changes-stack{max-height:46%;flex:none;border-bottom:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7832
+ '.dig-pane-changes{width:290px;flex:none;border-left:1px solid var(--dsw-alias-border-l2)}',
7833
+ '.dig-pane-changes-stack{max-height:46%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
7753
7834
  '.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-hairline,var(--dsw-alias-border-l1))}',
7835
+ '.dig-compact-tree{max-height:42%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
7755
7836
  // A divider: an 8px hit area (a fingertip needs it) painted as a 1px hairline
7756
7837
  // that lights up on hover, focus or while dragging. touch-action:none is what
7757
7838
  // makes a touch drag resize the pane instead of scrolling the panel.
@@ -7835,7 +7916,7 @@ window.__ModuleLoader__.load({
7835
7916
  '.dig-ref-remote{color:#b083f0}',
7836
7917
  '.dig-ref-tag{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary))}',
7837
7918
  '.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-hairline,var(--dsw-alias-border-l1))}',
7919
+ '.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
7920
  '.dig-filter-text{flex:1 1 90px;min-width:80px;width:auto}',
7840
7921
  '.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
7922
  '.dig-filter-select:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
@@ -7865,7 +7946,7 @@ window.__ModuleLoader__.load({
7865
7946
  '.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
7947
  '.dig-row:hover .dig-mini{opacity:1}',
7867
7948
  '.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-hairline,var(--dsw-alias-border-l1))}',
7949
+ '.dig-commit-box{flex:none;padding:6px;display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--dsw-alias-border-l2)}',
7869
7950
  '.dig-commit-box-compact{flex-direction:row;align-items:center;gap:6px;padding:4px 6px;flex-wrap:wrap}',
7870
7951
  '.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
7952
  '.dig-textarea::placeholder{color:var(--dsw-alias-label-tertiary)}',
@@ -7887,7 +7968,7 @@ window.__ModuleLoader__.load({
7887
7968
  '.dig-detail-files-head{font-weight:600;margin-top:4px}',
7888
7969
  '.dig-detail-files{display:flex;flex-direction:column}',
7889
7970
  '.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-hairline,var(--dsw-alias-border-l1))}',
7971
+ '.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
7972
  '.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
7973
  '.dig-diff-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}',
7893
7974
  '.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,7 +8065,17 @@ window.__ModuleLoader__.load({
7984
8065
  return undefined
7985
8066
  }
7986
8067
 
7987
- function ConversationPanel(props) {
8068
+ /* The conversation shell re-renders on EVERY keystroke (the composer owns
8069
+ the draft state), and the view area re-renders with it. Without a bail-out
8070
+ at this boundary that cascade dragged the ENTIRE Git panel tree — branch
8071
+ rows, graph lanes, changes — through a synchronous render per keypress
8072
+ while the Git view was the active one (typing lag, reported against
8073
+ v0.13.0). The component consumes exactly three stable props (sessionId,
8074
+ t, ctx); everything else the slot passes (inspectCall, viewRequest, …)
8075
+ changes identity per shell render and is deliberately ignored, so the
8076
+ comparator is: same session, same bindings → skip the subtree. cwd moves
8077
+ through the sessions subscription inside, not through props. */
8078
+ const ConversationPanel = memo(function ConversationPanel(props) {
7988
8079
  const ctx = props.ctx
7989
8080
  const sessionId = typeof props.sessionId === 'string' ? props.sessionId : ''
7990
8081
  const [cwd, setCwd] = useState(() => sessionCwdOf(ctx, sessionId))
@@ -7994,7 +8085,9 @@ window.__ModuleLoader__.load({
7994
8085
  sync()
7995
8086
  /* The session list is a live store; without a subscription a later
7996
8087
  start (first message in a blank session) would leave the tab on the
7997
- no-repo chrome until the next remount. */
8088
+ no-repo chrome until the next remount. The notify walks are cheap:
8089
+ getSnapshot + a string compare, and setCwd with an identical string
8090
+ bails out without a render. */
7998
8091
  let unsubscribe
7999
8092
  try {
8000
8093
  const sessions = ctx.get('sessions')
@@ -8010,7 +8103,7 @@ window.__ModuleLoader__.load({
8010
8103
  }, [ctx, sessionId])
8011
8104
  const scope = useMemo(() => ({ cwd: cwd, sessionId: sessionId }), [cwd, sessionId])
8012
8105
  return E(LocaleLive, { ctx: ctx, scope: scope, t: props.t, visible: true })
8013
- }
8106
+ }, (prev, next) => prev.sessionId === next.sessionId && prev.t === next.t && prev.ctx === next.ctx)
8014
8107
 
8015
8108
  function NativePanel(props) {
8016
8109
  const workspaces = typeof props.useWorkspaces === 'function'