@dsh-xhl/dsh-git-gui 0.1.3 → 0.1.5

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 +177 -75
  3. package/package.json +68 -68
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 added button in the bottom-left corner to open the sidebar. Then just enjoy the version control.
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): sidebar.footer.action 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
@@ -325,8 +325,20 @@ function icon(pathData, size) {
325
325
  }, h('path', { d: pathData }))
326
326
  }
327
327
 
328
+ /** Inline SVG icon (fill = currentColor, iconfont-style 1024 viewBox). */
329
+ function fillIcon(pathData, size) {
330
+ return h('svg', {
331
+ className: 'gg-icon',
332
+ width: size ?? 14,
333
+ height: size ?? 14,
334
+ viewBox: '0 0 1024 1024',
335
+ fill: 'currentColor',
336
+ 'aria-hidden': 'true',
337
+ }, h('path', { d: pathData }))
338
+ }
339
+
328
340
  const ICONS = {
329
- git: icon('M10.5 2.5 4 9m6.5-6.5 3 3m-3-3L8 0.5 16 8.5 7.5 17 0 9.5l8-8m6.5 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm-13 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z', 16),
341
+ git: fillIcon('M110.933333 451.84 357.546667 204.8 429.653333 277.333333C419.413333 313.6 436.053333 353.28 469.333333 372.48L469.333333 608.853333C443.733333 623.36 426.666667 651.093333 426.666667 682.666667 426.666667 729.6 465.066667 768 512 768 558.933333 768 597.333333 729.6 597.333333 682.666667 597.333333 651.093333 580.266667 623.36 554.666667 608.853333L554.666667 401.493333 642.986667 490.666667C640 497.066667 640 504.32 640 512 640 558.933333 678.4 597.333333 725.333333 597.333333 772.266667 597.333333 810.666667 558.933333 810.666667 512 810.666667 465.066667 772.266667 426.666667 725.333333 426.666667 717.653333 426.666667 710.4 426.666667 704 429.653333L594.346667 320C605.44 280.32 584.96 236.8 545.28 220.16 526.933333 213.333333 507.733333 211.626667 490.666667 216.32L418.133333 144.213333 451.84 110.933333C485.12 77.226667 538.88 77.226667 572.16 110.933333L913.066667 451.84C946.773333 485.12 946.773333 538.88 913.066667 572.16L572.16 913.066667C538.88 946.773333 485.12 946.773333 451.84 913.066667L110.933333 572.16C77.226667 538.88 77.226667 485.12 110.933333 451.84Z', 16),
330
342
  refresh: icon('M13.5 8a5.5 5.5 0 1 1-1.6-3.9M13.5 1.5v3h-3', 13),
331
343
  close: icon('M4 4l8 8M12 4l-8 8', 13),
332
344
  check: icon('M2.5 8.5 6 12l7.5-8', 13),
@@ -346,7 +358,7 @@ const ICONS = {
346
358
  globe: icon('M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13ZM1.5 8h13M8 1.5c1.8 1.8 2.7 4 2.7 6.5s-.9 4.7-2.7 6.5C6.2 12.7 5.3 10.5 5.3 8S6.2 3.3 8 1.5Z', 14),
347
359
  }
348
360
 
349
- module.exports = { h, cx, icon, ICONS, React }
361
+ module.exports = { h, cx, icon, fillIcon, ICONS, React }
350
362
 
351
363
  },
352
364
  "./i18n.js": function (module, exports, require) {
@@ -606,7 +618,7 @@ module.exports = { t, lang }
606
618
 
607
619
  const { startController } = require('./control')
608
620
  const { makeGitApi } = require('./api')
609
- const { FooterButton } = require('./v-footer')
621
+ const { HeaderButton, HeroGitButton } = require('./v-header')
610
622
  const { GitPanel } = require('./v-panel')
611
623
  const { css } = require('./styles')
612
624
  const PKG_ID = require('./pkg-id')
@@ -631,17 +643,27 @@ function apply(ctx) {
631
643
  startController(api)
632
644
 
633
645
  ctx.effect(() => {
634
- const disposeFooter = ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register(
635
- { name: 'sidebar.footer.action', id: 'dsh-git-gui', order: 20 },
636
- (props) => FooterButton(props),
637
- ))
638
- const disposePanel = ctx.slots.inject('shell.overlay', () => ctx.slots.register(
639
- { name: 'shell.overlay', id: 'dsh-git-gui' },
640
- (props) => GitPanel(props),
646
+ const disposeHeader = ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register(
647
+ { name: 'conversation.session.header.utilities', id: 'dsh-git-gui', order: 20 },
648
+ (props) => HeaderButton(props),
641
649
  ))
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
+ })
642
664
  return () => {
643
- disposeFooter()
644
- disposePanel()
665
+ disposeHeader()
666
+ disposeOverlay()
645
667
  }
646
668
  })
647
669
  }
@@ -1059,25 +1081,37 @@ body[data-ds-dark-theme] { --gg-on-accent: #10141b; }
1059
1081
  .gg-toast-err { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); }
1060
1082
  .gg-toast-warn { border-color: var(--dsw-alias-state-warn-primary); color: var(--dsw-alias-state-warn-primary); }
1061
1083
 
1062
- /* sidebar footer entry */
1063
- .gg-footer-btn {
1064
- display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;
1065
- border: none; background: transparent; cursor: pointer;
1066
- color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px;
1067
- padding: 0 6px; height: 36px; border-radius: 8px; position: relative;
1068
- }
1069
- .gg-footer-btn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
1070
- .gg-footer-btn.gg-active { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
1071
- .gg-footer-icon { display: inline-flex; align-items: center; justify-content: center; }
1072
- .gg-footer-marks { display: inline-flex; gap: 3px; align-items: center; }
1073
- .gg-badge {
1084
+ /* conversation header entry (top-right) */
1085
+ .gg-header-btn {
1086
+ position: relative;
1087
+ display: inline-flex; align-items: center; justify-content: center;
1088
+ width: 28px; height: 28px;
1089
+ border: .5px solid var(--dsw-alias-border-l3);
1090
+ border-radius: 14px;
1091
+ background: transparent;
1092
+ cursor: pointer;
1093
+ color: var(--dsw-alias-label-secondary);
1094
+ padding: 0;
1095
+ }
1096
+ .gg-header-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
1097
+ .gg-header-btn:active:not(:disabled) { background: var(--dsw-alias-interactive-bg-active); }
1098
+ .gg-header-btn.gg-active { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
1099
+ .gg-header-icon { display: inline-flex; align-items: center; justify-content: center; }
1100
+ .gg-header-btn .gg-badge {
1101
+ position: absolute; top: -4px; right: -4px;
1074
1102
  min-width: 15px; height: 15px; padding: 0 4px; border-radius: 999px;
1075
1103
  background: var(--dsw-alias-brand-primary); color: var(--gg-on-accent);
1076
1104
  font-size: 10px; font-weight: 700;
1077
1105
  display: inline-flex; align-items: center; justify-content: center;
1078
1106
  box-sizing: border-box;
1079
1107
  }
1080
- .gg-badge-err { background: var(--dsw-alias-state-error-primary); }
1108
+ .gg-header-btn .gg-badge-err { background: var(--dsw-alias-state-error-primary); }
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; }
1081
1115
 
1082
1116
  .gg-icon { flex: none; }
1083
1117
  `
@@ -1333,56 +1367,124 @@ function FilesView() {
1333
1367
  module.exports = { FilesView }
1334
1368
 
1335
1369
  },
1336
- "./v-footer.js": function (module, exports, require) {
1337
- /**
1338
- * Sidebar footer entry: a Git button with a changed-files badge.
1339
- * Registered into `sidebar.footer.action` (list / root scope).
1340
- */
1341
- const { h, cx, ICONS, React } = require('./dom')
1342
- const { useStore, setState } = require('./store')
1343
- const { t } = require('./i18n')
1344
- const { applySession } = require('./control')
1345
-
1346
- function changedCount(status) {
1347
- if (!status || !status.files) return 0
1348
- return status.files.length
1349
- }
1350
-
1351
- function FooterButton(props) {
1352
- const sessions = props.useSessions((s) => s)
1353
- React.useEffect(() => {
1354
- applySession(sessions)
1355
- }, [sessions])
1356
- const open = useStore((s) => s.open)
1357
- const count = useStore((s) => changedCount(s.status))
1358
- const statusError = useStore((s) => s.statusError)
1359
- const check = useStore((s) => s.check)
1360
- const wide = props.wide === true
1361
-
1362
- const title = t('panel.title')
1363
- const onClick = () => setState({ open: !open })
1364
-
1365
- const badge = count > 0
1366
- ? h('span', { className: 'gg-badge', title: `${count}` }, count > 99 ? '99+' : String(count))
1367
- : null
1368
- const dot = !badge && statusError && check?.repo === true
1369
- ? h('span', { className: 'gg-badge gg-badge-err', title: statusError }, '!')
1370
- : null
1371
-
1372
- return h('button', {
1373
- type: 'button',
1374
- className: cx('gg-footer-btn', open && 'gg-active'),
1375
- title,
1376
- onClick,
1377
- 'aria-label': title,
1378
- },
1379
- h('span', { className: 'gg-footer-icon' }, ICONS.git),
1380
- wide && h('span', { className: 'gg-footer-label' }, 'Git'),
1381
- (badge || dot) && h('span', { className: 'gg-footer-marks' }, badge, dot),
1382
- )
1383
- }
1384
-
1385
- module.exports = { FooterButton }
1370
+ "./v-header.js": function (module, exports, require) {
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
+
1454
+ React.useEffect(() => {
1455
+ const update = () => {
1456
+ const column = document.querySelector('[data-phase]')
1457
+ if (column === null || column.getAttribute('data-phase') !== 'hero') {
1458
+ setPos(null)
1459
+ return
1460
+ }
1461
+ const rect = column.getBoundingClientRect()
1462
+ if (rect.width === 0 || rect.height === 0) {
1463
+ setPos(null)
1464
+ return
1465
+ }
1466
+ // Mirror the session header utilities placement: the header pads
1467
+ // 12px top / 28px right and centers the 28px-tall group in its 32px
1468
+ // title row (→ 14px top), so the floating icon sits exactly where the
1469
+ // in-chat icon does.
1470
+ const top = rect.top + 14
1471
+ const right = window.innerWidth - rect.right + 28
1472
+ setPos((current) => current !== null && current.top === top && current.right === right ? current : { top, right })
1473
+ }
1474
+ const timer = window.setInterval(update, 400)
1475
+ window.addEventListener('resize', update)
1476
+ update()
1477
+ return () => {
1478
+ window.clearInterval(timer)
1479
+ window.removeEventListener('resize', update)
1480
+ }
1481
+ }, [])
1482
+
1483
+ if (pos === null) return null
1484
+ return h('div', { className: 'gg-hero-fab', style: { top: `${pos.top}px`, right: `${pos.right}px` } }, h(GitButton))
1485
+ }
1486
+
1487
+ module.exports = { HeaderButton, HeroGitButton, GitButton }
1386
1488
 
1387
1489
  },
1388
1490
  "./v-log.js": function (module, exports, require) {
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
- {
2
- "name": "@dsh-xhl/dsh-git-gui",
3
- "version": "0.1.3",
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
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": {
9
- "default": "./lib/index.js"
10
- },
11
- "./client": {
12
- "default": "./lib/client.js"
13
- },
14
- "./cordis.patch.yml": "./cordis.patch.yml",
15
- "./package.json": "./package.json"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/wojiaoxiaomayun/dsh-git-gui.git"
20
- },
21
- "files": [
22
- "lib/index.js",
23
- "lib/service.js",
24
- "lib/runner.js",
25
- "lib/parse.js",
26
- "lib/activity.js",
27
- "lib/client.js",
28
- "scripts/build-client.mjs",
29
- "package.json",
30
- "README.md",
31
- "cordis.patch.yml",
32
- "cordis.patch.example.yml"
33
- ],
34
- "dsh": {
35
- "bundle": {
36
- "patch": "./cordis.patch.yml"
37
- },
38
- "client": {
39
- "platform": "web",
40
- "inject": [
41
- "@deepseek-ai/dsh-client-connection",
42
- "@deepseek-ai/dsh-client-runtime",
43
- "@deepseek-ai/dsh-client-ui-sidebar"
44
- ]
45
- }
46
- },
47
- "scripts": {
48
- "build:client": "node scripts/build-client.mjs",
49
- "watch:client": "node scripts/build-client.mjs --watch",
50
- "test": "node --test",
51
- "prepack": "node scripts/build-client.mjs"
52
- },
53
- "peerDependencies": {
54
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
55
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6"
56
- },
57
- "devDependencies": {
58
- "@deepseek-ai/cordis": "^4.0.1",
59
- "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
60
- "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6",
61
- "react": "^18.3.1",
62
- "react-dom": "^18.3.1"
63
- },
64
- "license": "MIT",
65
- "publishConfig": {
66
- "access": "public"
67
- }
68
- }
1
+ {
2
+ "name": "@dsh-xhl/dsh-git-gui",
3
+ "version": "0.1.5",
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
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "default": "./lib/index.js"
10
+ },
11
+ "./client": {
12
+ "default": "./lib/client.js"
13
+ },
14
+ "./cordis.patch.yml": "./cordis.patch.yml",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/wojiaoxiaomayun/dsh-git-gui.git"
20
+ },
21
+ "files": [
22
+ "lib/index.js",
23
+ "lib/service.js",
24
+ "lib/runner.js",
25
+ "lib/parse.js",
26
+ "lib/activity.js",
27
+ "lib/client.js",
28
+ "scripts/build-client.mjs",
29
+ "package.json",
30
+ "README.md",
31
+ "cordis.patch.yml",
32
+ "cordis.patch.example.yml"
33
+ ],
34
+ "dsh": {
35
+ "bundle": {
36
+ "patch": "./cordis.patch.yml"
37
+ },
38
+ "client": {
39
+ "platform": "web",
40
+ "inject": [
41
+ "@deepseek-ai/dsh-client-connection",
42
+ "@deepseek-ai/dsh-client-runtime",
43
+ "@deepseek-ai/dsh-client-ui-conversation"
44
+ ]
45
+ }
46
+ },
47
+ "scripts": {
48
+ "build:client": "node scripts/build-client.mjs",
49
+ "watch:client": "node scripts/build-client.mjs --watch",
50
+ "test": "node --test",
51
+ "prepack": "node scripts/build-client.mjs"
52
+ },
53
+ "peerDependencies": {
54
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
55
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6"
56
+ },
57
+ "devDependencies": {
58
+ "@deepseek-ai/cordis": "^4.0.1",
59
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
60
+ "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6",
61
+ "react": "^18.3.1",
62
+ "react-dom": "^18.3.1"
63
+ },
64
+ "license": "MIT",
65
+ "publishConfig": {
66
+ "access": "public"
67
+ }
68
+ }