@shendeguize/dsh-agent-sidecar 0.1.0 → 0.2.0

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 (58) hide show
  1. package/README.md +78 -11
  2. package/lib/client.js +4708 -2932
  3. package/lib/client.js.map +1 -1
  4. package/package.json +3 -1
  5. package/src/client/analysis/AnalysisPanel.tsx +19 -27
  6. package/src/client/analysis/analysis.module.css +36 -97
  7. package/src/client/board/Board.tsx +255 -48
  8. package/src/client/board/board.module.css +113 -92
  9. package/src/client/board/logic.ts +99 -21
  10. package/src/client/board/project-view-logic.ts +27 -34
  11. package/src/client/board/project-view.module.css +46 -83
  12. package/src/client/board/project-view.tsx +50 -8
  13. package/src/client/board/strings.ts +70 -85
  14. package/src/client/commands.ts +30 -15
  15. package/src/client/controller.ts +27 -7
  16. package/src/client/detail/SessionDetail.tsx +234 -37
  17. package/src/client/detail/detail.module.css +100 -153
  18. package/src/client/detail/logic.ts +220 -29
  19. package/src/client/detail/strings.ts +66 -77
  20. package/src/client/detail-glue.ts +33 -0
  21. package/src/client/detail-view.module.css +43 -35
  22. package/src/client/detail-view.tsx +138 -118
  23. package/src/client/dsh-tools/LineageTree.tsx +22 -13
  24. package/src/client/dsh-tools/SearchPanel.tsx +18 -11
  25. package/src/client/dsh-tools/dsh-tools.module.css +53 -140
  26. package/src/client/dsh-tools/strings.ts +42 -77
  27. package/src/client/index.ts +285 -124
  28. package/src/client/inject/InjectPanel.tsx +81 -61
  29. package/src/client/inject/inject.module.css +97 -197
  30. package/src/client/inject/logic.ts +38 -0
  31. package/src/client/lifecycle/handoff.ts +83 -0
  32. package/src/client/locales/en.ts +91 -4
  33. package/src/client/locales/host.ts +131 -0
  34. package/src/client/locales/index.ts +196 -8
  35. package/src/client/locales/react.ts +10 -0
  36. package/src/client/locales/view.ts +38 -0
  37. package/src/client/locales/zh.ts +200 -125
  38. package/src/client/mount.tsx +75 -41
  39. package/src/client/navigation/CenterOverlay.tsx +40 -0
  40. package/src/client/navigation/center-overlay.module.css +51 -0
  41. package/src/client/navigation/center.ts +45 -0
  42. package/src/client/navigation/modal-isolation.ts +152 -0
  43. package/src/client/navigation/modal-surface-anchor.ts +72 -0
  44. package/src/client/navigation/sidebar-entry.module.css +73 -0
  45. package/src/client/navigation/sidebar-entry.ts +309 -0
  46. package/src/client/primitives/StaticPill.tsx +21 -0
  47. package/src/client/settings-card.module.css +35 -119
  48. package/src/client/settings-card.tsx +31 -153
  49. package/src/client/settings-fields.tsx +131 -0
  50. package/src/client/sidebar/SidebarTab.tsx +156 -0
  51. package/src/client/sidebar/model.ts +65 -0
  52. package/src/client/sidebar/sidebar-tab.module.css +118 -0
  53. package/src/client/sidebar-tab.tsx +46 -320
  54. package/src/client/theme/agsc.module.css +31 -0
  55. package/src/client/theme/parts.ts +56 -0
  56. package/src/client/ui-integration.ts +86 -0
  57. package/src/client/widget.tsx +42 -16
  58. package/src/client/inject/overlay.module.css +0 -22
@@ -0,0 +1,40 @@
1
+ import { Modal } from '@deepseek-ai/dsh-client-ui-primitives'
2
+ import { useRef } from 'react'
3
+ import type { ReactElement, ReactNode } from 'react'
4
+ import { surfaceProps } from '../theme/parts.ts'
5
+ import css from './center-overlay.module.css'
6
+ import { useModalIsolation } from './modal-isolation.ts'
7
+ import { useModalSurfaceAnchor } from './modal-surface-anchor.ts'
8
+
9
+ export interface CenterOverlayProps {
10
+ open: boolean
11
+ onClose: () => void
12
+ title: string
13
+ closeLabel: string
14
+ children?: ReactNode
15
+ }
16
+
17
+ /** Pure Agent Center presentation over the shell's frame-wide overlay seat. */
18
+ export function CenterOverlay(props: CenterOverlayProps): ReactElement {
19
+ const surfaceRef = useRef<HTMLDivElement>(null)
20
+ const surface = surfaceProps('overlay', css['dialog'])
21
+ useModalIsolation(props.open, surfaceRef)
22
+ useModalSurfaceAnchor(props.open, surfaceRef, surface)
23
+ return (
24
+ <Modal
25
+ open={props.open}
26
+ onClose={props.onClose}
27
+ title={props.title}
28
+ closeLabel={props.closeLabel}
29
+ className={surface.className}
30
+ contentClassName={css['content']}
31
+ >
32
+ <div
33
+ ref={surfaceRef}
34
+ className={css['surface']}
35
+ >
36
+ {props.children}
37
+ </div>
38
+ </Modal>
39
+ )
40
+ }
@@ -0,0 +1,51 @@
1
+ .dialog {
2
+ gap: 0;
3
+ width: min(1180px, calc(100vw - 48px));
4
+ height: min(860px, calc(100vh - 48px));
5
+ max-height: calc(100vh - 48px);
6
+ padding: 0;
7
+ border-color: var(--agsc-border-strong);
8
+ border-radius: var(--agsc-radius-card);
9
+ background: var(--agsc-bg);
10
+ box-shadow: var(--agsc-shadow-card);
11
+ }
12
+
13
+ .content {
14
+ min-height: 0;
15
+ height: 100%;
16
+ overflow: hidden;
17
+ }
18
+
19
+ .content > :last-child {
20
+ flex: 1;
21
+ min-height: 0;
22
+ margin-top: 0;
23
+ padding: 0;
24
+ overflow: hidden;
25
+ }
26
+
27
+ .surface {
28
+ display: flex;
29
+ flex-direction: column;
30
+ min-width: 0;
31
+ min-height: 0;
32
+ height: 100%;
33
+ color: var(--agsc-fg);
34
+ background: var(--agsc-bg);
35
+ overflow: hidden;
36
+ }
37
+
38
+ .surface > :last-child {
39
+ flex: 1;
40
+ min-height: 0;
41
+ height: auto;
42
+ }
43
+
44
+ @media (max-width: 720px) {
45
+ .dialog {
46
+ width: calc(100vw - 16px);
47
+ height: calc(100dvh - 16px);
48
+ max-height: calc(100dvh - 16px);
49
+ border-radius: var(--agsc-radius-control);
50
+ }
51
+ }
@@ -0,0 +1,45 @@
1
+ /** Callback port used by UI entry points that open Agent Center. */
2
+ export type CenterNavigation = () => boolean
3
+
4
+ /** Observable state port bound to the shell overlay at the composition root. */
5
+ export interface CenterNavigationStore {
6
+ readonly open: CenterNavigation
7
+ close(): void
8
+ subscribe(listener: () => void): () => void
9
+ getSnapshot(): boolean
10
+ }
11
+
12
+ /**
13
+ * Create one DOM-free navigation source for every Agent Center entry point.
14
+ * Opening is always accepted; a shell overlay that mounts later observes the
15
+ * retained snapshot instead of losing the request.
16
+ */
17
+ export function createCenterNavigation(): CenterNavigationStore {
18
+ let isOpen = false
19
+ const listeners = new Set<() => void>()
20
+ const notify = (): void => {
21
+ for (const listener of [...listeners]) listener()
22
+ }
23
+
24
+ const open: CenterNavigation = () => {
25
+ if (!isOpen) {
26
+ isOpen = true
27
+ notify()
28
+ }
29
+ return true
30
+ }
31
+
32
+ return {
33
+ open,
34
+ close: () => {
35
+ if (!isOpen) return
36
+ isOpen = false
37
+ notify()
38
+ },
39
+ subscribe: (listener) => {
40
+ listeners.add(listener)
41
+ return () => { listeners.delete(listener) }
42
+ },
43
+ getSnapshot: () => isOpen,
44
+ }
45
+ }
@@ -0,0 +1,152 @@
1
+ import { useEffect } from 'react'
2
+ import type { RefObject } from 'react'
3
+ const DIALOG_SELECTOR = '[role="dialog"][aria-modal="true"]'
4
+ const FOCUSABLE_SELECTOR = [
5
+ 'a[href],area[href],button:not([disabled])',
6
+ 'input:not([disabled]):not([type="hidden"]),select:not([disabled])',
7
+ 'textarea:not([disabled]),iframe,[contenteditable="true"],[tabindex]',
8
+ ].join(',')
9
+ function focusableElements(dialog: HTMLElement): HTMLElement[] {
10
+ return Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))
11
+ .filter((element) =>
12
+ element.tabIndex >= 0 &&
13
+ !element.hidden &&
14
+ element.closest('[inert],[aria-hidden="true"]') === null &&
15
+ element.getClientRects().length > 0)
16
+ }
17
+ function restorableIn(element: HTMLElement | null, dialog: HTMLElement): element is HTMLElement {
18
+ return element?.isConnected === true &&
19
+ dialog.contains(element) &&
20
+ !element.hidden &&
21
+ element.closest('[inert],[aria-hidden="true"]') === null &&
22
+ element.getClientRects().length > 0
23
+ }
24
+ type DialogFrame = { dialog: HTMLElement; opener: HTMLElement | null; focus: HTMLElement | null }
25
+ /** Isolate the active official Modal, including sibling-portaled nested dialogs. */
26
+ export function useModalIsolation(
27
+ open: boolean,
28
+ surfaceRef: RefObject<HTMLElement>,
29
+ ): void {
30
+ useEffect(() => {
31
+ if (!open || typeof document === 'undefined' || typeof window === 'undefined') return
32
+ const body = document.body
33
+ if (body === null || typeof window.MutationObserver === 'undefined') return
34
+ const previousFocus =
35
+ document.activeElement instanceof HTMLElement ? document.activeElement : null
36
+ const originalInert = new Map<HTMLElement, boolean>()
37
+ const dialogStack: DialogFrame[] = []
38
+ let topDialog: HTMLElement | null = null
39
+ let focusFrame: number | null = null
40
+ const getTopDialog = (): HTMLElement | null => {
41
+ const outer = surfaceRef.current?.closest<HTMLElement>(DIALOG_SELECTOR) ?? null
42
+ if (outer === null) return null
43
+ const dialogs = Array.from(body.querySelectorAll<HTMLElement>(DIALOG_SELECTOR))
44
+ const outerIndex = dialogs.indexOf(outer)
45
+ return outerIndex < 0 ? null : dialogs.at(-1) ?? null
46
+ }
47
+ const bodyBranch = (dialog: HTMLElement): HTMLElement | null => {
48
+ let branch: Node = dialog
49
+ while (branch.parentNode !== null && branch.parentNode !== body) {
50
+ branch = branch.parentNode
51
+ }
52
+ return branch instanceof HTMLElement ? branch : null
53
+ }
54
+ const isolateAround = (dialog: HTMLElement | null): void => {
55
+ const activeBranch = dialog === null ? null : bodyBranch(dialog)
56
+ const next = new Set(
57
+ Array.from(body.children)
58
+ .filter((element): element is HTMLElement =>
59
+ element instanceof HTMLElement && element !== activeBranch),
60
+ )
61
+ for (const [element, wasInert] of originalInert) {
62
+ if (next.has(element)) continue
63
+ element.inert = wasInert
64
+ originalInert.delete(element)
65
+ }
66
+ for (const element of next) {
67
+ if (!originalInert.has(element)) originalInert.set(element, element.inert)
68
+ element.inert = true
69
+ }
70
+ }
71
+ const queueFocus = (dialog: HTMLElement, preferred: HTMLElement | null = null): void => {
72
+ if (focusFrame !== null) window.cancelAnimationFrame(focusFrame)
73
+ focusFrame = window.requestAnimationFrame(() => {
74
+ focusFrame = null
75
+ if (getTopDialog() !== dialog) return
76
+ const target = restorableIn(preferred, dialog)
77
+ ? preferred
78
+ : focusableElements(dialog)[0]
79
+ target?.focus({ preventScroll: true })
80
+ })
81
+ }
82
+ const sync = (): void => {
83
+ const next = getTopDialog()
84
+ const changed = next !== topDialog
85
+ let closed: DialogFrame[] = []
86
+ if (changed && next !== null) {
87
+ const index = dialogStack.findIndex(frame => frame.dialog === next)
88
+ if (index < 0) {
89
+ const parent = dialogStack.at(-1)
90
+ const active =
91
+ document.activeElement instanceof HTMLElement ? document.activeElement : null
92
+ const opener = parent !== undefined && restorableIn(active, parent.dialog)
93
+ ? active
94
+ : parent?.focus ?? null
95
+ dialogStack.push({ dialog: next, opener, focus: null })
96
+ } else {
97
+ closed = dialogStack.splice(index + 1)
98
+ }
99
+ }
100
+ topDialog = next
101
+ isolateAround(next)
102
+ const opener = next === null
103
+ ? null
104
+ : closed.reverse().find(frame => restorableIn(frame.opener, next))?.opener ?? null
105
+ if (
106
+ next !== null &&
107
+ (changed || !next.contains(document.activeElement))
108
+ ) {
109
+ queueFocus(next, opener)
110
+ }
111
+ }
112
+ const onFocusIn = (event: FocusEvent): void => {
113
+ if (!(event.target instanceof HTMLElement)) return
114
+ const dialog = event.target.closest<HTMLElement>(DIALOG_SELECTOR)
115
+ const frame = dialogStack.find(item => item.dialog === dialog)
116
+ if (frame !== undefined) frame.focus = event.target
117
+ }
118
+ const onKeyDown = (event: KeyboardEvent): void => {
119
+ if (event.key !== 'Tab' || event.defaultPrevented) return
120
+ const dialog = getTopDialog()
121
+ if (dialog === null) return
122
+ if (dialog !== topDialog) sync()
123
+
124
+ const focusable = focusableElements(dialog)
125
+ if (focusable.length === 0) {
126
+ event.preventDefault()
127
+ return
128
+ }
129
+ const activeIndex = focusable.indexOf(document.activeElement as HTMLElement)
130
+ const wrapsBackward = event.shiftKey && activeIndex <= 0
131
+ const wrapsForward = !event.shiftKey && activeIndex === focusable.length - 1
132
+ if (activeIndex < 0 || wrapsBackward || wrapsForward) {
133
+ event.preventDefault()
134
+ const target = event.shiftKey ? focusable.at(-1) : focusable[0]
135
+ target?.focus({ preventScroll: true })
136
+ }
137
+ }
138
+ const observer = new window.MutationObserver(sync)
139
+ observer.observe(body, { childList: true, subtree: true })
140
+ document.addEventListener('focusin', onFocusIn)
141
+ document.addEventListener('keydown', onKeyDown, true)
142
+ sync()
143
+ return () => {
144
+ observer.disconnect()
145
+ document.removeEventListener('focusin', onFocusIn)
146
+ document.removeEventListener('keydown', onKeyDown, true)
147
+ if (focusFrame !== null) window.cancelAnimationFrame(focusFrame)
148
+ for (const [element, wasInert] of originalInert) element.inert = wasInert
149
+ if (previousFocus?.isConnected === true) previousFocus.focus({ preventScroll: true })
150
+ }
151
+ }, [open, surfaceRef])
152
+ }
@@ -0,0 +1,72 @@
1
+ import { useEffect, useLayoutEffect } from 'react'
2
+ import type { RefObject } from 'react'
3
+ import type { SurfaceProps } from '../theme/parts.ts'
4
+
5
+ const DIALOG_SELECTOR = '[role="dialog"][aria-modal="true"]'
6
+ const MODAL_SURFACE_OWNER = Symbol.for(
7
+ '@shendeguize/dsh-agent-sidecar/modal-surface-anchor-owner',
8
+ )
9
+
10
+ type ModalSurfaceAttributes = Pick<
11
+ SurfaceProps,
12
+ 'data-dsh-plugin' | 'data-dsh-part'
13
+ >
14
+
15
+ export interface ModalSurfaceAnchorTarget {
16
+ setAttribute(name: string, value: string): void
17
+ removeAttribute(name: string): void
18
+ }
19
+
20
+ /** Use a pre-paint effect in the browser without warning during SSR. */
21
+ export function selectModalSurfaceAnchorEffect(
22
+ hasDocument: boolean,
23
+ ): typeof useEffect {
24
+ return hasDocument ? useLayoutEffect : useEffect
25
+ }
26
+
27
+ const useIsomorphicLayoutEffect = selectModalSurfaceAnchorEffect(
28
+ typeof document !== 'undefined',
29
+ )
30
+
31
+ function ownerSlot(target: ModalSurfaceAnchorTarget): Record<PropertyKey, unknown> {
32
+ return target as unknown as Record<PropertyKey, unknown>
33
+ }
34
+
35
+ /** Attach the public surface attributes with latest-owner-safe cleanup. */
36
+ export function attachModalSurfaceAnchor(
37
+ target: ModalSurfaceAnchorTarget | null,
38
+ attributes: ModalSurfaceAttributes,
39
+ ): () => void {
40
+ if (target === null) return () => {}
41
+
42
+ const owner = Symbol('modal-surface-anchor')
43
+ const slot = ownerSlot(target)
44
+ slot[MODAL_SURFACE_OWNER] = owner
45
+ target.setAttribute('data-dsh-plugin', attributes['data-dsh-plugin'])
46
+ target.setAttribute('data-dsh-part', attributes['data-dsh-part'])
47
+
48
+ return () => {
49
+ if (slot[MODAL_SURFACE_OWNER] !== owner) return
50
+ target.removeAttribute('data-dsh-plugin')
51
+ target.removeAttribute('data-dsh-part')
52
+ delete slot[MODAL_SURFACE_OWNER]
53
+ }
54
+ }
55
+
56
+ /** Commit the public anchor onto the official Modal's real dialog element. */
57
+ export function useModalSurfaceAnchor(
58
+ open: boolean,
59
+ surfaceRef: RefObject<HTMLElement>,
60
+ attributes: ModalSurfaceAttributes,
61
+ ): void {
62
+ useIsomorphicLayoutEffect(() => {
63
+ if (!open || typeof document === 'undefined') return
64
+ const dialog = surfaceRef.current?.closest<HTMLElement>(DIALOG_SELECTOR) ?? null
65
+ return attachModalSurfaceAnchor(dialog, attributes)
66
+ }, [
67
+ open,
68
+ surfaceRef,
69
+ attributes['data-dsh-plugin'],
70
+ attributes['data-dsh-part'],
71
+ ])
72
+ }
@@ -0,0 +1,73 @@
1
+ .entry {
2
+ box-sizing: border-box;
3
+ display: flex;
4
+ align-items: center;
5
+ gap: 8px;
6
+ width: 100%;
7
+ height: 36px;
8
+ padding: 0 10px;
9
+ border: 0;
10
+ border-radius: var(--agsc-radius-control);
11
+ background: none;
12
+ color: var(--agsc-fg-secondary);
13
+ cursor: pointer;
14
+ font: inherit;
15
+ font-size: 13px;
16
+ text-align: left;
17
+ white-space: nowrap;
18
+ transition: background-color 120ms ease, color 120ms ease, transform 120ms ease;
19
+ }
20
+
21
+ .entry:hover {
22
+ background: var(--dsw-alias-interactive-bg-hover);
23
+ color: var(--agsc-fg);
24
+ }
25
+
26
+ .entry:active {
27
+ background: var(--dsw-alias-interactive-bg-active);
28
+ color: var(--agsc-fg);
29
+ transform: translateY(1px);
30
+ }
31
+
32
+ .entry:focus-visible {
33
+ outline: 2px solid var(--agsc-accent);
34
+ outline-offset: 2px;
35
+ }
36
+
37
+ .entryIcon {
38
+ display: inline-flex;
39
+ flex: none;
40
+ align-items: center;
41
+ justify-content: center;
42
+ width: 24px;
43
+ height: 24px;
44
+ }
45
+
46
+ .entryIcon svg {
47
+ display: block;
48
+ width: 16px;
49
+ height: 16px;
50
+ }
51
+
52
+ .entryLabel {
53
+ overflow: hidden;
54
+ text-overflow: ellipsis;
55
+ }
56
+
57
+ [data-dsh-frame][data-sidebar-collapsed] .entry {
58
+ justify-content: center;
59
+ width: 36px;
60
+ padding: 0;
61
+ margin: 0 auto 12px;
62
+ border-radius: 50%;
63
+ }
64
+
65
+ [data-dsh-frame][data-sidebar-collapsed] .entryLabel {
66
+ display: none;
67
+ }
68
+
69
+ @media (prefers-reduced-motion: reduce) {
70
+ .entry {
71
+ transition: none;
72
+ }
73
+ }