@dsh-xhl/dsh-git-gui 0.1.4 → 0.1.6

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 (3) hide show
  1. package/README.md +2 -2
  2. package/lib/client.js +157 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -75,7 +75,7 @@ If you do want a `link:` dev loop, run `scripts/link-host-deps.ps1` after every
75
75
 
76
76
  ## Where to start
77
77
 
78
- After installing the plugin, open the DeepSeek Harness Web UI and click the Git button (with a changed-files count badge) in the top-right corner of the conversation header to open the floating panel.
78
+ After installing the plugin, open the DeepSeek Harness Web UI and click the Git button (with a changed-files count badge) in the top-right corner of the conversation header to open the floating panel. On the hero / new-chat page (before a conversation exists) the same button is shown pinned to the same top-right spot.
79
79
 
80
80
  ## Structure
81
81
 
@@ -85,7 +85,7 @@ Host (Node): GitService (@Remote, namespace `git`, Typert SRC mode → git/* en
85
85
  ├─ parse.js porcelain v2 / unified diff / log / refs / stash parsing
86
86
  └─ activity.js session/event → (session, turn, tool, file) timeline (Stage 2)
87
87
 
88
- Client (Browser): conversation.session.header.utilities entry button (uncommitted-count badge) + shell.overlay floating panel
88
+ Client (Browser): conversation.session.header.utilities entry button (uncommitted-count badge) + shell.overlay floating panel + hero-page floating button
89
89
  ├─ control.js session cwd synchronization + polling + operation runner + confirmation dialogs
90
90
  ├─ v-*.js view components (React.createElement, no JSX)
91
91
  └─ styles.js theme tokens (--dsw-alias-*) and styles, adaptive to light/dark mode
package/lib/client.js CHANGED
@@ -618,7 +618,7 @@ module.exports = { t, lang }
618
618
 
619
619
  const { startController } = require('./control')
620
620
  const { makeGitApi } = require('./api')
621
- const { HeaderButton } = require('./v-header')
621
+ const { HeaderButton, HeroGitButton } = require('./v-header')
622
622
  const { GitPanel } = require('./v-panel')
623
623
  const { css } = require('./styles')
624
624
  const PKG_ID = require('./pkg-id')
@@ -647,13 +647,23 @@ function apply(ctx) {
647
647
  { name: 'conversation.session.header.utilities', id: 'dsh-git-gui', order: 20 },
648
648
  (props) => HeaderButton(props),
649
649
  ))
650
- const disposePanel = ctx.slots.inject('shell.overlay', () => ctx.slots.register(
651
- { name: 'shell.overlay', id: 'dsh-git-gui' },
652
- (props) => GitPanel(props),
653
- ))
650
+ const disposeOverlay = ctx.slots.inject('shell.overlay', () => {
651
+ const disposePanel = ctx.slots.register(
652
+ { name: 'shell.overlay', id: 'dsh-git-gui' },
653
+ (props) => GitPanel(props),
654
+ )
655
+ const disposeHero = ctx.slots.register(
656
+ { name: 'shell.overlay', id: 'dsh-git-gui-hero', order: 90 },
657
+ (props) => HeroGitButton(props),
658
+ )
659
+ return () => {
660
+ disposePanel()
661
+ disposeHero()
662
+ }
663
+ })
654
664
  return () => {
655
665
  disposeHeader()
656
- disposePanel()
666
+ disposeOverlay()
657
667
  }
658
668
  })
659
669
  }
@@ -1097,6 +1107,12 @@ body[data-ds-dark-theme] { --gg-on-accent: #10141b; }
1097
1107
  }
1098
1108
  .gg-header-btn .gg-badge-err { background: var(--dsw-alias-state-error-primary); }
1099
1109
 
1110
+ /* hero / blank-session floating entry: pinned over the conversation column's
1111
+ top-right corner while the column is in its hero phase. No explicit
1112
+ z-index keeps it below the plugin's own floating panel and sibling overlay
1113
+ entries (modal, notices) — mirroring the file-editor hero FAB. */
1114
+ .gg-hero-fab { position: fixed; pointer-events: auto; }
1115
+
1100
1116
  .gg-icon { flex: none; }
1101
1117
  `
1102
1118
 
@@ -1352,56 +1368,141 @@ module.exports = { FilesView }
1352
1368
 
1353
1369
  },
1354
1370
  "./v-header.js": function (module, exports, require) {
1355
- /**
1356
- * Conversation header entry: a single compact Git button with a
1357
- * changed-files badge (角标), registered into
1358
- * `conversation.session.header.utilities` (session scope, top-right).
1359
- */
1360
- const { h, cx, ICONS, React } = require('./dom')
1361
- const { useStore, setState } = require('./store')
1362
- const { t } = require('./i18n')
1363
- const { applySession } = require('./control')
1364
-
1365
- function changedCount(status) {
1366
- if (!status || !status.files) return 0
1367
- return status.files.length
1368
- }
1369
-
1370
- function HeaderButton(props) {
1371
- // session-scoped slot: bind the active session's workspace via useSessions.
1372
- const sessions = props.useSessions((s) => s)
1373
- React.useEffect(() => {
1374
- applySession(sessions)
1375
- }, [sessions])
1376
- const open = useStore((s) => s.open)
1377
- const count = useStore((s) => changedCount(s.status))
1378
- const statusError = useStore((s) => s.statusError)
1379
- const check = useStore((s) => s.check)
1380
-
1381
- const title = t('panel.title')
1382
- const onClick = () => setState({ open: !open })
1383
-
1384
- const badge = count > 0
1385
- ? h('span', { className: 'gg-badge', title: `${count}` }, count > 99 ? '99+' : String(count))
1386
- : null
1387
- const dot = !badge && statusError && check?.repo === true
1388
- ? h('span', { className: 'gg-badge gg-badge-err', title: statusError }, '!')
1389
- : null
1390
-
1391
- return h('button', {
1392
- type: 'button',
1393
- className: cx('gg-header-btn', open && 'gg-active'),
1394
- title,
1395
- onClick,
1396
- 'aria-label': title,
1397
- },
1398
- h('span', { className: 'gg-header-icon' }, ICONS.git),
1399
- badge,
1400
- dot,
1401
- )
1402
- }
1403
-
1404
- module.exports = { HeaderButton }
1371
+ /**
1372
+ * Conversation header entry: a single compact Git button with a
1373
+ * changed-files badge (角标), registered into
1374
+ * `conversation.session.header.utilities` (session scope, top-right).
1375
+ * A hero twin is rendered by the plugin itself through the generic
1376
+ * `shell.overlay` layer, pinned to the same spot while the conversation
1377
+ * column is in its hero phase (no session / blank-session hero), where the
1378
+ * session header utilities are absent or deliberately hidden.
1379
+ */
1380
+ const { h, cx, ICONS, React } = require('./dom')
1381
+ const { useStore, setState } = require('./store')
1382
+ const { t } = require('./i18n')
1383
+ const { applySession } = require('./control')
1384
+
1385
+ function changedCount(status) {
1386
+ if (!status || !status.files) return 0
1387
+ return status.files.length
1388
+ }
1389
+
1390
+ /**
1391
+ * The compact Git icon button + badge. Pure presentational: reads only the
1392
+ * plugin's own store (stable hooks), so it can be mounted both in the
1393
+ * session header utilities and in the hero overlay twin without depending on
1394
+ * framework-scoped session hooks (whose availability can change across
1395
+ * renders in the hero, which would trip React's rules-of-hooks check).
1396
+ */
1397
+ function GitButton() {
1398
+ const open = useStore((s) => s.open)
1399
+ const count = useStore((s) => changedCount(s.status))
1400
+ const statusError = useStore((s) => s.statusError)
1401
+ const check = useStore((s) => s.check)
1402
+
1403
+ const title = t('panel.title')
1404
+ const onClick = () => setState({ open: !open })
1405
+
1406
+ const badge = count > 0
1407
+ ? h('span', { className: 'gg-badge', title: `${count}` }, count > 99 ? '99+' : String(count))
1408
+ : null
1409
+ const dot = !badge && statusError && check?.repo === true
1410
+ ? h('span', { className: 'gg-badge gg-badge-err', title: statusError }, '!')
1411
+ : null
1412
+
1413
+ return h('button', {
1414
+ type: 'button',
1415
+ className: cx('gg-header-btn', open && 'gg-active'),
1416
+ title,
1417
+ onClick,
1418
+ 'aria-label': title,
1419
+ },
1420
+ h('span', { className: 'gg-header-icon' }, ICONS.git),
1421
+ badge,
1422
+ dot,
1423
+ )
1424
+ }
1425
+
1426
+ /**
1427
+ * Session header entry: binds the active session's workspace to the plugin
1428
+ * store via the session-scoped `useSessions` hook, then renders GitButton.
1429
+ * `useSessions` is required here (session-scoped slot always provides it).
1430
+ */
1431
+ function HeaderButton(props) {
1432
+ const sessions = props.useSessions((s) => s)
1433
+ React.useEffect(() => {
1434
+ if (sessions) applySession(sessions)
1435
+ }, [sessions])
1436
+ return h(GitButton)
1437
+ }
1438
+
1439
+ /**
1440
+ * Hero / blank-session floating Git entry: the same compact button, mounted
1441
+ * through the generic `shell.overlay` layer and pinned to the conversation
1442
+ * column's top-right corner — the spot where the header utilities sit once a
1443
+ * conversation has records. Renders nothing while the column is not in its
1444
+ * `hero` phase (so it never overlaps the in-chat header button or the
1445
+ * floating panel). It deliberately does NOT subscribe to the framework
1446
+ * sessions hook — in the hero there may be no session at all, and a hook
1447
+ * whose presence flips between renders would trip React's rules-of-hooks
1448
+ * invariant (#310). The plugin store already holds the last-known workspace,
1449
+ * which is what the button's badge reflects.
1450
+ */
1451
+ function HeroGitButton() {
1452
+ const [pos, setPos] = React.useState(null)
1453
+ const selfRef = React.useRef(null)
1454
+
1455
+ React.useEffect(() => {
1456
+ const update = () => {
1457
+ const column = document.querySelector('[data-phase]')
1458
+ if (column === null || column.getAttribute('data-phase') !== 'hero') {
1459
+ setPos(null)
1460
+ return
1461
+ }
1462
+ const rect = column.getBoundingClientRect()
1463
+ if (rect.width === 0 || rect.height === 0) {
1464
+ setPos(null)
1465
+ return
1466
+ }
1467
+ // Mirror the session header utilities placement: the header pads
1468
+ // 12px top / 28px right and centers the 28px-tall group in its 32px
1469
+ // title row (→ 14px top), so the floating icon sits exactly where the
1470
+ // in-chat icon does.
1471
+ const top = rect.top + 14
1472
+ // If other plugins also pin a hero FAB to this corner (e.g. the
1473
+ // file-editor's filex-hero-fab), stack ours to the LEFT of them so the
1474
+ // two entries sit side by side instead of overlapping. Only elements
1475
+ // already in the DOM count; our own fab (data-gg-hero-fab) is skipped.
1476
+ let extra = 0
1477
+ const fabNodes = document.querySelectorAll('[class*="hero-fab"]')
1478
+ for (const node of fabNodes) {
1479
+ if (node === selfRef.current) continue
1480
+ if (node.getAttribute('data-gg-hero-fab') !== null) continue
1481
+ const w = node.offsetWidth
1482
+ if (w > 0) extra += w + 8
1483
+ }
1484
+ const right = window.innerWidth - rect.right + 28 + extra
1485
+ setPos((current) => current !== null && current.top === top && current.right === right ? current : { top, right })
1486
+ }
1487
+ const timer = window.setInterval(update, 400)
1488
+ window.addEventListener('resize', update)
1489
+ update()
1490
+ return () => {
1491
+ window.clearInterval(timer)
1492
+ window.removeEventListener('resize', update)
1493
+ }
1494
+ }, [])
1495
+
1496
+ if (pos === null) return null
1497
+ return h('div', {
1498
+ ref: selfRef,
1499
+ 'data-gg-hero-fab': '',
1500
+ className: 'gg-hero-fab',
1501
+ style: { top: `${pos.top}px`, right: `${pos.right}px` },
1502
+ }, h(GitButton))
1503
+ }
1504
+
1505
+ module.exports = { HeaderButton, HeroGitButton, GitButton }
1405
1506
 
1406
1507
  },
1407
1508
  "./v-log.js": function (module, exports, require) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-xhl/dsh-git-gui",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Git GUI panel for the DeepSeek Harness web surface: visualize working-tree changes (including every modification the agent makes), stage/commit/branch/merge/log/pull/push/stash/revert — all from the browser.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",