@lijian-ui/dsh-file-manager 0.2.1 → 0.2.3

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.
@@ -33,17 +33,13 @@ export interface ConversationService {
33
33
  export interface FileReference {
34
34
  /** Workspace-relative path (what the agent will read). */
35
35
  path: string;
36
- /** First line of the referenced range (1-based, inclusive). */
37
- startLine: number;
38
- /** Last line of the referenced range (1-based, inclusive). */
39
- endLine: number;
36
+ /** First line of the referenced range (1-based, inclusive). Omit for whole-file references. */
37
+ startLine?: number;
38
+ /** Last line of the referenced range (1-based, inclusive). Omit for whole-file references. */
39
+ endLine?: number;
40
40
  }
41
- /** The `@path:lines` model form (what the codec serializes to on submit). */
42
- export declare function formatFileRefToken(ref: {
43
- path: string;
44
- startLine: number;
45
- endLine: number;
46
- }): string;
41
+ /** The `@path` / `@path:lines` model form (what the codec serializes to on submit). */
42
+ export declare function formatFileRefToken(ref: FileReference): string;
47
43
  /** The DRAFT-REV+CAS span the conversation machine expects for chip inserts. */
48
44
  export interface RefSpan {
49
45
  start: number;
@@ -1 +1 @@
1
- {"version":3,"file":"reference.d.ts","sourceRoot":"","sources":["../../../src/client/reference.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAA;AAEtF;;;;;;;;;;GAUG;AAEH;;;;GAIG;AACH,UAAU,UAAU;IAClB,KAAK,EAAE;QAAE,WAAW,IAAI;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAA;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AACD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,CAAA;KAAE,CAAA;CAChD;AAED,4DAA4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAA;IACZ,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAA;CAChB;AA0BD,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAGpG;AAUD,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAezG;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,OAAO,GACZ,OAAO,CAkBT;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,GAAG,EAAE,aAAa,GACjB,OAAO,CAcT"}
1
+ {"version":3,"file":"reference.d.ts","sourceRoot":"","sources":["../../../src/client/reference.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAA;AAEtF;;;;;;;;;;GAUG;AAEH;;;;GAIG;AACH,UAAU,UAAU;IAClB,KAAK,EAAE;QAAE,WAAW,IAAI;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAA;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AACD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,CAAA;KAAE,CAAA;CAChD;AAED,4DAA4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAA;IACZ,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AA2BD,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAI7D;AAYD,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAezG;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,OAAO,GACZ,OAAO,CAmBT;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,aAAa,EAClB,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,GAAG,EAAE,aAAa,GACjB,OAAO,CAcT"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lijian-ui/dsh-file-manager",
3
3
  "description": "DSH web GUI right-panel system: a pixel-faithful re-implementation of FileManager's Explorer + Preview columns (file tree, filename search, git changes, multi-tab preview of 10+ formats) driven by real host-side fs/git services, hot-pluggable via a profile bundle without any dsh source changes",
4
- "version": "0.2.1",
4
+ "version": "0.2.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -14,7 +14,7 @@ function ensureStyle(): void {
14
14
  if (document.getElementById(STYLE_ID) !== null) return
15
15
  const tag = document.createElement('style')
16
16
  tag.id = STYLE_ID
17
- tag.textContent = '.dsh-dock-item{width:32px;height:32px;border:none;border-radius:9999px;background:#000;color:#fff;cursor:pointer;position:relative;transition:transform .15s ease,background .18s ease;display:inline-flex;align-items:center;justify-content:center}.dsh-dock-item:hover{transform:scale(1.15);background:#1a1a1a}.dsh-dock-item-active{background:var(--aion-primary,#6ea0ff)}.dsh-dock-tooltip{position:absolute;top:calc(100% + 6px);left:50%;transform:translateX(-50%);padding:2px 8px;font-size:12px;line-height:1.4;white-space:nowrap;color:#fff;background:rgba(20,20,30,.9);border:1px solid rgba(255,255,255,.12);border-radius:6px;opacity:0;pointer-events:none;transition:opacity .15s}.dsh-dock-item:hover .dsh-dock-tooltip{opacity:1}'
17
+ tag.textContent = '.dsh-dock-item{width:32px;height:32px;border:none;border-radius:9999px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-base);cursor:pointer;position:relative;transition:transform .15s ease,background .18s ease;display:inline-flex;align-items:center;justify-content:center}.dsh-dock-item:hover{transform:scale(1.15);background:color-mix(in srgb,var(--dsw-alias-label-primary) 85%,var(--dsw-alias-bg-base))}.dsh-dock-item-active{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-bg-base)}.dsh-dock-tooltip{position:absolute;top:calc(100% + 6px);left:50%;transform:translateX(-50%);padding:2px 8px;font-size:12px;line-height:1.4;white-space:nowrap;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;opacity:0;pointer-events:none;transition:opacity .15s}.dsh-dock-item:hover .dsh-dock-tooltip{opacity:1}'
18
18
  document.head.appendChild(tag)
19
19
  }
20
20
 
@@ -267,7 +267,7 @@ function SearchArea({
267
267
  <div className={explorerCss.searchArea}>
268
268
  <div
269
269
  className={`${explorerCss.searchBox}${searchFocus ? ` ${explorerCss.searchAreaFocus}` : ''}`}
270
- style={{ borderColor: searchFocus ? 'var(--aion-primary)' : undefined }}
270
+ style={{ borderColor: searchFocus ? 'var(--dsw-alias-state-business-primary)' : undefined }}
271
271
  >
272
272
  <span className={explorerCss.searchIcon}><SearchIcon size={14} /></span>
273
273
  <input
@@ -130,7 +130,7 @@ export function ScmPanel({ stores }: { stores: PanelStores }): JSX.Element {
130
130
  </span>
131
131
  <span className={scmCss.sectionTitle}>{t('scm.changes')}</span>
132
132
  {status.branch !== '' && (
133
- <span className={scmCss.branchName} style={{ fontSize: 11, color: 'var(--aion-text-tertiary)', display: 'flex', alignItems: 'center', gap: 4 }}>
133
+ <span className={scmCss.branchName} style={{ fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', display: 'flex', alignItems: 'center', gap: 4 }}>
134
134
  <BranchIcon size={12} />
135
135
  {status.branch}
136
136
  </span>
@@ -161,7 +161,7 @@ export function ScmPanel({ stores }: { stores: PanelStores }): JSX.Element {
161
161
  type="button"
162
162
  className={scmCss.sectionAction}
163
163
  title={t('scm.viewList')}
164
- style={{ color: state.viewMode === 'list' ? 'var(--aion-brand)' : undefined }}
164
+ style={{ color: state.viewMode === 'list' ? 'var(--dsw-alias-brand-primary)' : undefined }}
165
165
  onClick={() => scm.setViewMode('list')}
166
166
  >
167
167
  <ListIcon size={13} />
@@ -170,7 +170,7 @@ export function ScmPanel({ stores }: { stores: PanelStores }): JSX.Element {
170
170
  type="button"
171
171
  className={scmCss.sectionAction}
172
172
  title={t('scm.viewTree')}
173
- style={{ color: state.viewMode === 'tree' ? 'var(--aion-brand)' : undefined }}
173
+ style={{ color: state.viewMode === 'tree' ? 'var(--dsw-alias-brand-primary)' : undefined }}
174
174
  onClick={() => scm.setViewMode('tree')}
175
175
  >
176
176
  <TreeIcon size={13} />
@@ -359,7 +359,7 @@ function DirNode({
359
359
  <ChevronRightIcon size={13} />
360
360
  </span>
361
361
  <FileTypeIcon name={label} isDir expanded={expanded} />
362
- <span style={{ fontSize: 13, color: 'var(--aion-text-primary)' }}>{label}</span>
362
+ <span style={{ fontSize: 13, color: 'var(--dsw-alias-label-primary)' }}>{label}</span>
363
363
  </div>
364
364
  {expanded &&
365
365
  rows.map((row) => (
@@ -37,7 +37,6 @@ import { handleFileRefClick } from './chat/file-ref.ts'
37
37
  import { appendToDraft, appendFileReferenceChip } from './reference.ts'
38
38
  import { registerFileReferenceSource } from './file-source.ts'
39
39
  import type { FileReferenceSelection } from './preview/selection.ts'
40
- import './styles/chip.module.css'
41
40
  import { FilePanelDockItem } from './FilePanelDockItem.tsx'
42
41
 
43
42
  declare module '@deepseek-ai/dsh-client-ui-slots' {
@@ -223,9 +222,8 @@ export function apply(ctx: ClientContext): void {
223
222
  let lastPreviewOpen = false
224
223
 
225
224
  // The Explorer row '@' button: insert a file-reference chip for files
226
- // (path + 1..N) or plain `@<rel-path>/` text for folders. File lines
227
- // are fetched from the host so the chip can carry an accurate line
228
- // range; failures fall back to plain text.
225
+ // (plain `@<rel-path>`, no line range) or plain `@<rel-path>/` text for
226
+ // folders.
229
227
  const onReference = (path: string, isDir: boolean): void => {
230
228
  const sessionId = ctx.sessions.list.getSnapshot().current as SessionId | undefined
231
229
  if (sessionId === undefined) {
@@ -236,16 +234,7 @@ export function apply(ctx: ClientContext): void {
236
234
  appendToDraft(ctx, sessionId, `@${path}/ `)
237
235
  return
238
236
  }
239
- void panelApi.read(currentRoot, path, false).then((result) => {
240
- if (!result.ok) {
241
- appendToDraft(ctx, sessionId, `@${path} `)
242
- return
243
- }
244
- const text = result.value.content
245
- const trimmed = text.endsWith('\n') ? text.slice(0, -1) : text
246
- const lines = trimmed === '' ? 1 : trimmed.split('\n').length
247
- appendFileReferenceChip(ctx, sessionId, { path, startLine: 1, endLine: lines })
248
- })
237
+ appendFileReferenceChip(ctx, sessionId, { path })
249
238
  }
250
239
 
251
240
  // The preview "添加到对话" action: insert a file-reference chip
@@ -165,7 +165,7 @@ export class PanelLayoutController {
165
165
  previewCol.style.overflow = 'hidden'
166
166
  previewCol.style.display = 'flex'
167
167
  previewCol.style.flexDirection = 'column'
168
- previewCol.style.borderLeft = '1px solid var(--aion-bg-3, #e5e6eb)'
168
+ previewCol.style.borderLeft = '1px solid var(--dsw-alias-bg-layer-3)'
169
169
 
170
170
  const explorerCol = document.createElement('div')
171
171
  explorerCol.dataset.filemgrExplorerCol = ''
@@ -174,7 +174,7 @@ export class PanelLayoutController {
174
174
  explorerCol.style.overflow = 'hidden'
175
175
  explorerCol.style.display = 'flex'
176
176
  explorerCol.style.flexDirection = 'column'
177
- explorerCol.style.borderLeft = '1px solid var(--aion-bg-3, #e5e6eb)'
177
+ explorerCol.style.borderLeft = '1px solid var(--dsw-alias-bg-layer-3)'
178
178
 
179
179
  frame.appendChild(previewCol)
180
180
  frame.appendChild(explorerCol)
@@ -4,13 +4,15 @@
4
4
  *
5
5
  * Trigger: typing `@` (with nothing after it) in the composer opens this
6
6
  * modal directly — no inline trigger menu (the shell's trigger menu is a
7
- * flat candidate list that cannot host a tree). The modal owns a recursive
8
- * tree with lazy expansion, a workspace filename search, per-row checkboxes
9
- * for multi-selection, and a chip row of the picked paths.
7
+ * flat candidate list that cannot host a tree). On open the just-typed `@`
8
+ * is stripped from the composer draft, which permanently silences the
9
+ * shell's own draft-driven `@` menu while the picker is up. The modal owns
10
+ * a recursive tree with lazy expansion, a workspace filename search,
11
+ * per-row checkboxes for multi-selection, and a chip row of the picked
12
+ * paths.
10
13
  *
11
- * Insertion replaces the `@` token the picker was opened from with the
12
- * picked references joined by spaces (`@a.ts @src/ @b.md`); cancel drops
13
- * the stray `@`.
14
+ * Insertion lands the picked references at the recorded anchor position
15
+ * (`@a.ts @src/ @b.md`); cancel drops the stray `@`.
14
16
  * @module dsh-filemgr/client/picker/FilePickerModal
15
17
  */
16
18
 
@@ -399,6 +401,28 @@ export function FilePickerHost({ ctx, api }: { ctx: ClientContext; api: PanelApi
399
401
  initialDir: '',
400
402
  span: { start: mention.start, end: mention.start + 1 },
401
403
  })
404
+ // Strip the just-typed `@` from the composer draft. Ordering note: this
405
+ // listener runs on the textarea (target phase) BEFORE the shell's
406
+ // root-delegated handler commits the keystroke into the machine, so a
407
+ // synchronous strip would read a STALE snapshot (guard fails, `@`
408
+ // survives). Defer to a microtask — by then the machine has committed
409
+ // the draft with the `@`, the guard holds, and the shell's draft-driven
410
+ // `@` menu loses its anchor for good. insert() re-checks the anchor
411
+ // idempotently in case this race ever lands the other way.
412
+ queueMicrotask(() => {
413
+ try {
414
+ const actx = ctx.sessions.scope(sessionId)
415
+ const conversation = (ctx as unknown as { conversation?: ConversationService }).conversation
416
+ if (actx === undefined || conversation === undefined) return
417
+ const shellInput = conversation.input.for(actx)
418
+ const snap = shellInput.state.getSnapshot()
419
+ if (snap.draft.charAt(mention.start) === '@') {
420
+ shellInput.setDraft(snap.draft.slice(0, mention.start) + snap.draft.slice(mention.start + 1))
421
+ }
422
+ } catch {
423
+ // Best-effort: insert() re-checks the anchor idempotently.
424
+ }
425
+ })
402
426
  }
403
427
  const attach = (): void => {
404
428
  for (const ta of document.querySelectorAll<HTMLTextAreaElement>('textarea')) {
@@ -421,58 +445,53 @@ export function FilePickerHost({ ctx, api }: { ctx: ClientContext; api: PanelApi
421
445
  /**
422
446
  * Insert the picked references into the composer draft as file-reference
423
447
  * chips (one `slash/input-insert-reference` bail per file; the `file`
424
- * source codec serializes each to `@path:lines` on submit). Folders fall
425
- * back to plain `@path/ ` text since the chip has no line concept.
426
- * Multiple selections land as adjacent chips.
448
+ * source codec serializes each to `@path` on submit). Whole-file picks
449
+ * carry no line range. Folders fall back to plain `@path/ ` text since
450
+ * the chip has no directory concept. Multiple selections land as
451
+ * adjacent chips. Since open stripped the bare `@` from the draft, every
452
+ * chip inserts at the recorded anchor as a zero-length span.
427
453
  */
428
454
  const insert = async (paths: PickedPath[]): Promise<void> => {
429
455
  const current = filePicker.getSnapshot()
430
456
  if (!current.open || current.req === null || paths.length === 0) return
431
- const { sessionId, root, span } = current.req
457
+ const { sessionId, span } = current.req
432
458
  try {
433
459
  const actx = ctx.sessions.scope(sessionId)
434
460
  if (actx === undefined) return
435
461
  const conversation = (ctx as unknown as { conversation?: ConversationService }).conversation
436
462
  if (conversation === undefined) return
437
463
  const shell = conversation.input.for(actx)
438
- // Pre-fetch every file's line count in parallel (bails are serial so
439
- // they share the same draft; reads are independent).
440
- const lineCounts = await Promise.all(paths.map(async (pick) => {
441
- if (pick.isDir) return { pick, lines: null }
442
- const result = await api.read(root, pick.path, false)
443
- if (!result.ok) return { pick, lines: null }
444
- const text = result.value.content
445
- const trimmed = text.endsWith('\n') ? text.slice(0, -1) : text
446
- const lines = trimmed === '' ? 1 : trimmed.split('\n').length
447
- return { pick, lines }
448
- }))
449
- // Walk in original order. The first chip replaces the `@` token; the
450
- // rest insert at the current cursor (0-length span) so the machine
451
- // appends each one in turn.
452
- let cursor = span.end
453
- let firstSpan = true
454
- for (const { pick, lines } of lineCounts) {
455
- if (pick.isDir) {
456
- appendToDraft(ctx, sessionId, `@${pick.path}/ `)
457
- continue
464
+ // Idempotent anchor cleanup: the open-time microtask strip usually
465
+ // removed the bare `@` already; if that race ever landed the other way
466
+ // (or was skipped), remove it now so no stray `@` survives next to the
467
+ // inserted chips.
468
+ {
469
+ const snap0 = shell.state.getSnapshot()
470
+ if (snap0.draft.charAt(span.start) === '@') {
471
+ shell.setDraft(snap0.draft.slice(0, span.start) + snap0.draft.slice(span.start + 1))
458
472
  }
459
- if (lines === null) continue
473
+ }
474
+ // Walk in REVERSE pick order, every file chip at the SAME zero-length
475
+ // anchor: each machine insert splices the chip's DISPLAY TEXT (`@label`)
476
+ // plus a gap at span.start — the chip look itself is a minted scan
477
+ // decoration, never special characters — pushing earlier-inserted text
478
+ // to the right, so the draft reads in original pick order with no
479
+ // cursor bookkeeping. (A fixed advance of 2 would land the next chip
480
+ // mid-text of the previous one.) Folders keep the plain-text append at
481
+ // the draft tail.
482
+ for (let i = paths.length - 1; i >= 0; i--) {
483
+ const pick = paths[i]
484
+ if (pick.isDir) continue
460
485
  const snap = shell.state.getSnapshot()
461
- const currentSpan: RefSpan = firstSpan
462
- ? { start: span.start, end: span.end, draftRev: snap.draftRev }
463
- : { start: cursor, end: cursor, draftRev: snap.draftRev }
464
- const ok = insertFileReference(
486
+ insertFileReference(
465
487
  ctx,
466
488
  sessionId,
467
- { path: pick.path, startLine: 1, endLine: lines },
468
- currentSpan,
489
+ { path: pick.path },
490
+ { start: span.start, end: span.start, draftRev: snap.draftRev },
469
491
  )
470
- if (ok) {
471
- firstSpan = false
472
- // The machine replaces [start,end) with U+FFFC and an optional
473
- // trailing space; the cursor advances by 2 for the new characters.
474
- cursor += 2
475
- }
492
+ }
493
+ for (const pick of paths) {
494
+ if (pick.isDir) appendToDraft(ctx, sessionId, `@${pick.path}/ `)
476
495
  }
477
496
  filePicker.close()
478
497
  } catch (error) {
@@ -70,6 +70,7 @@ export function PreviewPanel({ stores, onAddFileReference }: { stores: PanelStor
70
70
  }
71
71
  const ref = findFileReferenceSelection(el, selection, activeTab)
72
72
  if (ref === null) {
73
+
73
74
  setAddToChat(null)
74
75
  return
75
76
  }
@@ -246,7 +246,7 @@ function HtmlViewer({
246
246
  }): JSX.Element {
247
247
  const srcDoc = useMemo(() => {
248
248
  // Base styles so the embedded page inherits the theme background.
249
- return `<!doctype html><html><head><meta charset="utf-8"><style>body{margin:0;font-family:-apple-system,"system-ui","Segoe UI",Roboto,"PingFang SC",sans-serif;color:#1d2129}@media (prefers-color-scheme:dark){body{color:rgba(255,255,255,0.9)}}</style></head><body>${content}</body></html>`
249
+ return `<!doctype html><html><head><meta charset="utf-8"><style>body{margin:0;font-family:-apple-system,"system-ui","Segoe UI",Roboto,"PingFang SC",sans-serif;color:var(--dsw-alias-label-primary)}@media (prefers-color-scheme:dark){body{color:var(--dsw-alias-label-secondary)}}</style></head><body>${content}</body></html>`
250
250
  }, [content])
251
251
  if (sourceMode && onContentChange !== undefined) {
252
252
  return (
@@ -29,10 +29,10 @@ export interface ConversationService {
29
29
  export interface FileReference {
30
30
  /** Workspace-relative path (what the agent will read). */
31
31
  path: string
32
- /** First line of the referenced range (1-based, inclusive). */
33
- startLine: number
34
- /** Last line of the referenced range (1-based, inclusive). */
35
- endLine: number
32
+ /** First line of the referenced range (1-based, inclusive). Omit for whole-file references. */
33
+ startLine?: number
34
+ /** Last line of the referenced range (1-based, inclusive). Omit for whole-file references. */
35
+ endLine?: number
36
36
  }
37
37
 
38
38
  /** Basename of a `/`-separated relative path ('' when empty). */
@@ -56,11 +56,13 @@ function formatRefLabel(ref: FileReference): string {
56
56
  const basename = basenameOf(ref.path)
57
57
  const dirname = dirnameOf(ref.path)
58
58
  const name = dirname === '' ? basename : `${basename} - ${dirname}`
59
+ if (ref.startLine === undefined || ref.endLine === undefined) return name
59
60
  return ref.endLine > ref.startLine ? `${name} ${ref.startLine}-${ref.endLine}` : `${name} ${ref.startLine}`
60
61
  }
61
62
 
62
- /** The `@path:lines` model form (what the codec serializes to on submit). */
63
- export function formatFileRefToken(ref: { path: string; startLine: number; endLine: number }): string {
63
+ /** The `@path` / `@path:lines` model form (what the codec serializes to on submit). */
64
+ export function formatFileRefToken(ref: FileReference): string {
65
+ if (ref.startLine === undefined || ref.endLine === undefined) return `@${ref.path}`
64
66
  const range = ref.endLine > ref.startLine ? `${ref.startLine}-${ref.endLine}` : `${ref.startLine}`
65
67
  return `@${ref.path}:${range}`
66
68
  }
@@ -71,6 +73,8 @@ interface ConversationReference {
71
73
  ref: string
72
74
  label: string
73
75
  clipboardText: string
76
+ /** Official chip appearance: renders the reference icon over the `@` glyph. */
77
+ appearance: 'file'
74
78
  }
75
79
 
76
80
  /** The DRAFT-REV+CAS span the conversation machine expects for chip inserts. */
@@ -86,7 +90,7 @@ export function appendToDraft(ctx: ClientContext, sessionId: SessionId | undefin
86
90
  if (sessionId === undefined) return false
87
91
  const actx = ctx.sessions.scope(sessionId)
88
92
  if (actx === undefined) return false
89
- const conversation = (ctx as unknown as { conversation?: ConversationService }).conversation
93
+ const conversation = (ctx as unknown as { get(name: string): unknown }).get('conversation') as ConversationService | undefined
90
94
  if (conversation === undefined) return false
91
95
  const input = conversation.input.for(actx as unknown as ClientContext)
92
96
  const draft = input.state.getSnapshot().draft
@@ -122,6 +126,7 @@ export function insertFileReference(
122
126
  ref: formatFileRefToken(ref),
123
127
  label: formatRefLabel(ref),
124
128
  clipboardText: formatFileRefToken(ref),
129
+ appearance: 'file',
125
130
  }
126
131
  return (actx as unknown as {
127
132
  bail(target: unknown, event: string, payload: unknown): boolean
@@ -148,7 +153,7 @@ export function appendFileReferenceChip(
148
153
  if (sessionId === undefined) return false
149
154
  const actx = ctx.sessions.scope(sessionId)
150
155
  if (actx === undefined) return false
151
- const conversation = (ctx as unknown as { conversation?: ConversationService }).conversation
156
+ const conversation = (ctx as unknown as { get(name: string): unknown }).get('conversation') as ConversationService | undefined
152
157
  if (conversation === undefined) return false
153
158
  const shell = conversation.input.for(actx as unknown as ClientContext)
154
159
  const snap = shell.state.getSnapshot()
@@ -8,7 +8,7 @@
8
8
  .overlay {
9
9
  position: fixed;
10
10
  inset: 0;
11
- background: rgba(0, 0, 0, 0.45);
11
+ background: var(--dsw-alias-bg-mask-1);
12
12
  display: flex;
13
13
  align-items: center;
14
14
  justify-content: center;
@@ -24,7 +24,7 @@
24
24
  background: var(--aion-bg-base);
25
25
  border: 1px solid var(--aion-border-base);
26
26
  border-radius: 10px;
27
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
27
+ box-shadow: var(--dsw-shadow-lv3);
28
28
  overflow: hidden;
29
29
  font-family: var(--aion-font-sans);
30
30
  color: var(--aion-text-primary);
@@ -158,11 +158,11 @@
158
158
  }
159
159
 
160
160
  .rowSelected {
161
- background: rgba(22, 93, 255, 0.12);
161
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent);
162
162
  }
163
163
 
164
164
  .rowSelected:hover {
165
- background: rgba(22, 93, 255, 0.18);
165
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent);
166
166
  }
167
167
 
168
168
  .chev {
@@ -220,7 +220,7 @@
220
220
  height: 16px;
221
221
  border: 1px solid var(--aion-border-base);
222
222
  border-radius: 4px;
223
- color: #fff;
223
+ color: var(--dsw-alias-label-primary-foreground);
224
224
  flex-shrink: 0;
225
225
  background: transparent;
226
226
  }
@@ -294,7 +294,7 @@
294
294
  }
295
295
 
296
296
  .chipX:hover {
297
- background: rgba(0, 0, 0, 0.12);
297
+ background: var(--dsw-alias-interactive-bg-hover);
298
298
  color: var(--aion-text-primary);
299
299
  }
300
300
 
@@ -325,8 +325,8 @@
325
325
  height: 32px;
326
326
  padding: 0 16px;
327
327
  border: none;
328
- background: var(--aion-primary);
329
- color: #fff;
328
+ background: var(--dsw-alias-button-primary-fill);
329
+ color: var(--dsw-alias-label-primary-foreground);
330
330
  border-radius: 6px;
331
331
  font-size: 13px;
332
332
  font-weight: 500;
@@ -335,7 +335,7 @@
335
335
  }
336
336
 
337
337
  .insertBtn:hover:not(:disabled) {
338
- filter: brightness(1.1);
338
+ background: var(--dsw-alias-button-primary-hover);
339
339
  }
340
340
 
341
341
  .insertBtn:disabled {
@@ -504,25 +504,30 @@
504
504
  border-radius: 4px;
505
505
  }
506
506
 
507
- /* The "添加到对话" floating action over a preview selection. */
507
+ /* The "添加到对话" floating action over a preview selection.
508
+ * Mirrors the official primitives Button primary variant (.sm capsule):
509
+ * geometry h28/r14/p10, fill = button-primary-fill, text = label-primary-foreground,
510
+ * hover = button-primary-hover, shadow = --dsw-shadow-lv1 (floating layer). */
508
511
  .addToChat {
509
512
  position: fixed;
510
513
  z-index: 1000;
511
- padding: 6px 12px;
512
- background: var(--aion-primary);
513
- color: #fff;
514
- border-radius: 6px;
515
- font-size: 13px;
516
- font-weight: 500;
517
- font-family: var(--aion-font-sans);
514
+ display: inline-flex;
515
+ align-items: center;
516
+ height: 28px;
517
+ padding: 0 10px;
518
+ background: var(--dsw-alias-button-primary-fill);
519
+ color: var(--dsw-alias-label-primary-foreground);
520
+ border-radius: 14px;
521
+ font-size: 12px;
522
+ line-height: 18px;
518
523
  cursor: pointer;
519
- box-shadow: var(--aion-overlay-shadow);
524
+ box-shadow: var(--dsw-shadow-lv1);
520
525
  user-select: none;
521
526
  white-space: nowrap;
522
527
  }
523
528
 
524
529
  .addToChat:hover {
525
- filter: brightness(1.1);
530
+ background: var(--dsw-alias-button-primary-hover);
526
531
  }
527
532
 
528
533
  /* Code viewer shell: line gutter + code body in one scroll container. */