fix(editor): keep file basename in @-reference links

The explorer @ button appended plain text (@folder/file.ts), which DSH's
composer only decorates as a folder ref (@folder/) — the file name stayed
outside the link. Insert FILE references through DSH's own structured
insert-reference event instead, minting one chip for the whole reference
(displayed as @basename, serialized as the full @path). Directories keep the
plain @dir/ mention so folder completion still works.
This commit is contained in:
Ch3n4y
2026-09-03 10:00:13 +08:00
committed by Menghuan1918
parent 396017b459
commit 94ffac0224
8 changed files with 129 additions and 21 deletions
+69 -1
View File
@@ -5,6 +5,14 @@
* through `ctx.get` (the inject-free read the app's own plugins use); a
* missing service or scope degrades to a logged no-op, never a crash.
*
* File references additionally use DSH's own structured insert event
* (`slash/input-insert-reference`, see `insertFileReference`) instead of
* plain draft text: the native `@file` picker emits this event, and the
* conversation input machine mints one occurrence whose chip covers the
* whole reference. Plain text `@folder/file.ts` only ever gets DSH's
* folder-ref decoration (`@folder/`) — the file name stays undecorated — so
* plain append is the fallback, not the primary path, for files.
*
* Insert position (fixes upstream issue #425): the draft store only exposes
* the whole string (`getSnapshot().draft` + `setDraft(text)`) — there is no
* caret API on the conversation service. The composer's `<textarea>` keeps
@@ -184,4 +192,64 @@ export function appendToDraft(ctx: Context, sessionId: string, text: string): bo
console.warn('[dsh-better-sidebar] draft insert failed:', error)
return false
}
}
}
/**
* The DSH `@file` spelling for one relative path, mirroring the host grammar
* (`formatFileMention` in `@deepseek-ai/dsh-file-reference`): plain when
* there is no whitespace, quoted when there is, and `undefined` when the
* path contains a control character or an embedded quote the editor grammar
* cannot represent.
*/
export function fileMention(relativePath: string): { mention: string; label: string } | undefined {
const path = relativePath.replace(/[\\/]+$/, '')
if (/[\u0000-\u001f\u007f-\u009f"]/u.test(path)) return undefined
const mention = /\s/u.test(path) ? `@"${path}"` : `@${path}`
const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
const label = at === -1 ? path : path.slice(at + 1)
return { mention, label }
}
/**
* Insert one FILE reference as a structured chip (like DSH's own `@` picker).
* The chip displays `@<basename>` but serializes to `@<relative path>` on
* send, so the reference stays a single link from trigger to basename.
*
* Directories are NOT handled here: DSH's folder grammar wants the trailing
* slash as plain text (`@dir/`) so completion can descend, which
* `appendToDraft` already covers.
*/
export function insertFileReference(ctx: Context, sessionId: string, relativePath: string): boolean {
const reference = fileMention(relativePath)
if (reference === undefined) return false
try {
const actx = ctx.sessions.scope(sessionId)
if (actx === undefined) return false
const conversation = ctx.get('conversation') as SidebarConversation | undefined
if (conversation === undefined) return false
const input = conversation.input.for(actx)
const before = input.state.getSnapshot()
if (before.draftRev === undefined) return false
// The session-scope Context's typed `emit` is keyed to DSH's closed event
// map; this internal composer event is deliberately string-loose at runtime.
;(actx as unknown as { emit(name: string, payload: unknown): void }).emit('slash/input-insert-reference', {
reference: {
source: 'reference',
ref: reference.mention,
label: reference.label,
appearance: 'file',
clipboardText: reference.mention,
},
span: {
draftRev: before.draftRev,
start: before.draft.length,
end: before.draft.length,
},
})
const after = input.state.getSnapshot()
return after.draftRev !== before.draftRev
} catch (error) {
console.warn('[dsh-better-sidebar] file-reference insert failed:', error)
return false
}
}