feat(dsh): adapt to DSH 0.1.5-alpha.1

Move the plugin onto the DSH 0.1.5-alpha.1 baseline (peer floor
^0.1.5-alpha.1, CI pinned to the alpha dist-tag) and fix the two host
contract changes that alpha line introduced.

Baseline:
- all @deepseek-ai/dsh-* peer/dev deps -> 0.1.5-alpha.1; optional peer
  @deepseek-ai/dsh-client-ui-sidebar-right (declared, unused until the
  native-surface phase); devDep @deepseek-ai/dsh-agent-loop (inbox fold)
- dsh.plugin.json engines >=0.1.5-alpha.1; version 0.19.0-alpha.0;
  SIDEBAR_SERVICE_VERSION follows

Live assistant stream (assistant/chunk is gone):
- 0.1.5 publishes in-flight model deltas as process-local
  agent/assistant-stream frames instead of durable assistant/chunk events
  and embeds the settled stream in assistant/message / assistant/attempt.
  src/assistant-live.ts folds the frames into a bounded per-session buffer;
  sidechat.events serves the current attempt as `live` rows (replaced every
  poll, never a delta) and the transcript renders them; the inherited
  in-progress snapshot reads message content / attempt stream / live frames.

Cold session reads (persistence.inspect is gone):
- 0.1.5 reads a stored session through an explicit read handle
  (open(id,'read') -> handle.read() -> close()). src/session-store.ts owns
  that protocol; every cold read (sidechat transcript, cold-resume preset,
  session.cwd, changes replay) goes through it.

Dead code and docs:
- drop the remote.session.openWorkspacePath interception: 0.1.5's chat opens
  files through ctx.sidebarRight.openResource instead, so the wrapper had no
  caller and would hijack the host's "open in app" gesture; the
  interceptOpenPath pref, its settings row and its locale keys go with it
- AGENTS.md §3 + external-plugin-guide.md describe the new baseline
- vitest setup bridges Node's localStorage accessor to jsdom's store (the
  pre-existing 26 failures on Node >= 22)
- desktop-layout e2e seeds through host RPC and types into the Lexical
  composer (0.1.5 replaced the textarea) instead of driving the directory
  picker

Verified: pnpm typecheck / lint / build / test (124 files, 1327 tests) /
check:consumer-types, plus pnpm test:mount against a real
@deepseek-ai/dsh@0.1.5-alpha.1 host (18/18).
This commit is contained in:
Menghuan1918
2026-09-09 11:15:30 +08:00
parent d88dcfc3a5
commit ba0114749f
59 changed files with 1163 additions and 1011 deletions
-11
View File
@@ -901,17 +901,6 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
<span className={css.suffix}>{t('settingsWidthSuffix')}</span>
</span>
</div>
<div className={css.row}>
<span className={css.rowText}>
<span className={css.title}>{t('settingsOpenPathTitle')}</span>
<span className={css.desc}>{t('settingsOpenPathDesc')}</span>
</span>
<Switch
label={t('settingsOpenPathTitle')}
checked={prefs.interceptOpenPath}
onChange={(next) => { applyPref({ interceptOpenPath: next }) }}
/>
</div>
<div className={css.row}>
<span className={css.rowText}>
<span className={css.title}>{t('settingsOpenToolsTitle')}</span>
+28 -5
View File
@@ -58,6 +58,7 @@ import {
sideThreadRows,
threadHasCompletedTurn,
threadTrailingPending,
type SidechatLiveEvent,
type SidechatThreadInfo,
} from '../sidechat-core.ts'
import {
@@ -112,6 +113,8 @@ const inFlightStarts = new Set<string>()
* the afterSeq delta and never re-download what they already hold). */
interface ThreadCache {
entries: SidebarHistoryEntry[]
/** The CURRENT attempt's live deltas (replaced every pull, never merged). */
live: SidechatLiveEvent[]
}
/** Row-render labels (locale-dependent, memoized once per mount). */
@@ -383,7 +386,7 @@ export function SideChatView(props: {
const [info, setInfo] = useState<SidechatThreadInfo | null>(null)
const [menuOpen, setMenuOpen] = useState(false)
const cacheRef = useRef<ThreadCache>({ entries: [] })
const cacheRef = useRef<ThreadCache>({ entries: [], live: [] })
// The previous poll's rows (see the mapping's reuse pass below).
const prevRowsRef = useRef<SidechatTranscriptRow[]>([])
const controllerRef = useRef<AbortController | null>(null)
@@ -457,14 +460,18 @@ export function SideChatView(props: {
try {
const cache = cacheRef.current
const afterSeq = cache.entries.at(-1)?.event.seq
const { events } = await api.sidechatEvents(childId, afterSeq, controller.signal)
const { events, live } = await api.sidechatEvents(childId, afterSeq, controller.signal)
// The live set is the CURRENT attempt, so it replaces whatever the
// previous pull held; a settled step drops out of the host buffer and
// its durable assistant/message takes over in the mapping.
cache.live = live ?? []
if (events.length > 0) {
// Wire events arrive as parsed JSON; the mirror narrows data to the
// record the mapping reads.
const incoming = events.map(event => ({ event: event as SidebarSessionEvent }))
cache.entries = mergeBySeq(cache.entries, incoming)
setRevision(value => value + 1)
}
setRevision(value => value + 1)
} catch {
// Aborted by a newer pull or a wire failure: keep the last rows.
}
@@ -482,7 +489,7 @@ export function SideChatView(props: {
// Reset the transcript cache whenever the binding changes, then focus
// the composer — it owns the first message of a fresh thread.
useEffect(() => {
cacheRef.current = { entries: [] }
cacheRef.current = { entries: [], live: [] }
prevRowsRef.current = []
controllerRef.current?.abort()
setError(null)
@@ -532,8 +539,24 @@ export function SideChatView(props: {
// The previous poll's rows ride into the mapping so unchanged rows keep
// their object identity (see reuseRows): the 2s poll re-renders only the
// changed tail instead of re-parsing markdown for the whole transcript.
// The live deltas are appended AFTER the durable events (they are the
// in-flight tail), and a delta whose step already settled durably is
// dropped: the durable message is authoritative and would otherwise render
// twice.
const rows = useMemo(() => {
const next = threadId === undefined ? [] : transcriptRows(cacheRef.current.entries, prevRowsRef.current)
const cache = cacheRef.current
const settled = new Set(
cache.entries
.filter(entry => entry.event.type === 'assistant/message')
.map(entry => {
const data = entry.event.data as { turn?: unknown; step?: unknown }
return `${String(data.turn)}:${String(data.step)}`
}),
)
const live = cache.live
.filter(event => !settled.has(`${String(event.data.turn)}:${String(event.data.step)}`))
.map(event => ({ event: event as SidebarSessionEvent }))
const next = threadId === undefined ? [] : transcriptRows([...cache.entries, ...live], prevRowsRef.current)
prevRowsRef.current = next
return next
},
+6 -3
View File
@@ -8,7 +8,7 @@
*/
import { encodeHtmlUrl } from '../html-route.ts'
import type { LastActivity } from '../subagent-activity.ts'
import type { SidechatLogEvent, SidechatThreadInfo } from '../sidechat-core.ts'
import type { SidechatLiveEvent, SidechatLogEvent, SidechatThreadInfo } from '../sidechat-core.ts'
import type { SidebarSessionEvent } from '../context-types.ts'
import type { BrowserProbeResult } from './browser.ts'
@@ -390,9 +390,12 @@ export const api = {
call<SidechatThreadInfo>('sidechat.info', { childId }),
/** One transcript pull of a Side Chat thread: the thread's OWN events
* (the inherited seed is cut host-side and never crosses the wire).
* `afterSeq` narrows the response to the delta beyond it (poll tail). */
* `afterSeq` narrows the response to the delta beyond it (poll tail).
* `live` is the thread's in-flight model deltas (DSH 0.1.5 publishes them
* outside the session log) — the CURRENT attempt on every pull, never a
* delta, so the caller replaces its live set instead of appending. */
sidechatEvents: (childId: string, afterSeq?: number, signal?: AbortSignal) =>
call<{ events: SidechatLogEvent[] }>('sidechat.events', {
call<{ events: SidechatLogEvent[]; live: SidechatLiveEvent[] }>('sidechat.events', {
childId,
...(afterSeq !== undefined ? { afterSeq } : {}),
}, signal),
+1 -13
View File
@@ -17,7 +17,7 @@ import { revalidateChunksOnReactivate, setChunkModuleSystem } from './chunk-load
import { registerBuiltins } from './builtins/index.ts'
import { Sidebar } from './Sidebar.tsx'
import { RenderBoundary } from './RenderBoundary.tsx'
import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx'
import { registerTurnTailInterception } from './intercept.tsx'
import { registerLinkInterception } from './link-intercept.ts'
import { registerImeGuard } from './ime-guard.ts'
import { registerSettingsNavIcon } from './settings-nav-icon.ts'
@@ -342,18 +342,6 @@ export function apply(ctx: Context): void {
'dsh-better-sidebar: turn-tail interception',
)
ctx.effect(
() => {
try {
return registerOpenPathInterception(ctx, sidebarStore)
} catch (error) {
fail('interception', error)
return () => {}
}
},
'dsh-better-sidebar: open-path interception',
)
ctx.effect(
() => {
try {
+1 -46
View File
@@ -11,7 +11,6 @@ import type { Context } from '../context-types.ts'
import { firstLeaf, revealPaths, togglePanel, type SidebarStore } from './state.ts'
import { t } from './locales.ts'
import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts'
import { wrapOpenWorkspacePath, type OpenWorkspacePathService } from './openpath-intercept.ts'
import css from './sidebar.module.css'
/** Open a file in the sidebar's editor (used by the intercepted row and the explorer). */
@@ -25,13 +24,6 @@ export function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: st
ctx.get('betterSidebar')?.openTab({ type: 'editor', title, path: absolute, id: `editor:${absolute}` })
}
/**
* The produced files the turn-tail selector last matched for the visible
* session. The "Show in folder" gesture carries no file path of its own
* (`'.'`), so the reveal highlights exactly these rows when available.
*/
let lastProduced: readonly string[] = []
/**
* Reveal the produced files in the sidebar explorer: expand their parent
* directories, highlight the rows, and focus the explorer tab (expanding the
@@ -137,9 +129,7 @@ export function registerTurnTailInterception(ctx: Context, store: SidebarStore):
select: (owner) => {
if (store.getSuspended()) return null
if (store.getPrefs().tabsEnabled['editor'] === false) return null
const matched = selectProducedFiles(owner)
if (matched !== null) lastProduced = matched
return matched
return selectProducedFiles(owner)
},
priority: -1,
registrant: 'dsh-better-sidebar',
@@ -149,38 +139,3 @@ export function registerTurnTailInterception(ctx: Context, store: SidebarStore):
}),
}, SidebarProducedFiles))
}
/**
* Register the chat file-open interception: shadows
* `remote.session.openWorkspacePath` — the single funnel every chat-side
* file open goes through on alpha hosts (tool-row path links, the
* produced-files row, prose mentions, inline-code paths) — so opens land in
* the sidebar editor instead of the Host OS. The folder-reveal gesture
* ("Show in folder" passes `'.'`) is the one exception: it is routed to the
* explorer. Gated by BOTH the `interceptOpenPath` pref and the editor tab's
* enable switch; declined opens fall through to the original remote call.
*
* The `remote.session` namespace service mounts asynchronously (the gateway
* client creates it when the session-controller contribution arrives) and
* is recreated on contribution remounts, so the wrapper installs through
* `ctx.inject`: the callback runs once the service exists and re-runs after
* every remount, re-applying the shadow on the fresh instance. Returns the
* disposer (disposes the inject fiber, which restores the original method
* descriptor — HMR-safe).
*/
export function registerOpenPathInterception(ctx: Context, store: SidebarStore): () => void {
const fiber = ctx.inject(['remote.session'], (fctx) => {
fctx.effect(() => {
const service = fctx.get('remote.session') as OpenWorkspacePathService
return wrapOpenWorkspacePath(service, {
takeoverEnabled: () => !store.getSuspended()
&& store.getPrefs().interceptOpenPath !== false
&& store.getPrefs().tabsEnabled['editor'] !== false,
currentSessionId: () => ctx.sessions.list.getSnapshot().current,
openInSidebar: (path, sessionId) => { openSidebarFile(ctx, store, sessionId, path) },
revealInExplorer: (_path, sessionId) => { revealInExplorer(ctx, store, sessionId, lastProduced) },
})
}, 'dsh-better-sidebar: open-path interception wrap')
})
return () => { void fiber.dispose() }
}
-2
View File
@@ -238,8 +238,6 @@ export const ar: Record<string, string> = {
settingsWidthTitle: 'الحصة الافتراضية للعرض',
settingsWidthDesc: 'الحصة الافتراضية للبطاقة الجانبية من عرض النافذة للمحادثات الجديدة (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'فتح ملفات المحادثة في الشريط الجانبي',
settingsOpenPathDesc: 'فتح روابط الملفات في المحادثة (صفوف الأدوات، الملفات الناتجة، الإشارات) في محرّر الشريط الجانبي بدلاً من التطبيق الافتراضي للنظام',
settingsOpenToolsTitle: 'حقن أداة فتح الشريط الجانبي للنموذج',
settingsOpenToolsDesc: 'عند التفعيل، يمكن للنموذج فتح الملفات والمجلدات وصفحات HTTP(S) في الشريط الجانبي عبر أداة sidebar_open (معطّل افتراضياً)',
settingsTitleBarTitle: 'وضع توافق الموضع',
-2
View File
@@ -223,8 +223,6 @@ export const de: Record<string, string> = {
settingsWidthTitle: 'Standardbreite',
settingsWidthDesc: 'Standardanteil der Seitenkarte an der Fensterbreite für neue Unterhaltungen (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Chat-Dateien in der Seitenleiste öffnen',
settingsOpenPathDesc: 'Dateilinks im Chat (Werkzeugzeilen, erstellte Dateien, Erwähnungen) werden im Seitenleisten-Editor geöffnet statt in der System-Standardanwendung',
settingsOpenToolsTitle: 'Seitenleisten-Öffnungswerkzeug für das Modell bereitstellen',
settingsOpenToolsDesc: 'Wenn aktiviert, kann das Modell über das sidebar_open-Werkzeug Dateien, Ordner und HTTP(S)-Seiten in der Seitenleiste öffnen (standardmäßig deaktiviert)',
settingsTitleBarTitle: 'Kompatibilitätsmodus der Position',
-2
View File
@@ -230,8 +230,6 @@ export const fr: Record<string, string> = {
settingsWidthTitle: 'Part de largeur par défaut',
settingsWidthDesc: 'Pourcentage de la largeur de la fenêtre occupé par la carte latérale pour les nouvelles sessions (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Ouvrir les fichiers de discussion dans la barre latérale',
settingsOpenPathDesc: 'À la place de l’application par défaut du système, ouvrir dans l’éditeur de la barre latérale les liens vers les fichiers du chat (lignes d’outils, listes de produits, mentions de fichiers)',
settingsOpenToolsTitle: 'Injecter l\'outil d\'ouverture latérale pour le modèle',
settingsOpenToolsDesc: 'Une fois activé, le modèle peut ouvrir des fichiers, dossiers et pages HTTP(S) dans la barre latérale via l\'outil sidebar_open (désactivé par défaut)',
settingsTitleBarTitle: 'Mode de compatibilité de position',
-2
View File
@@ -237,8 +237,6 @@ export const hi: Record<string, string> = {
settingsWidthTitle: 'डिफ़ॉल्ट चौड़ाई हिस्सा',
settingsWidthDesc: 'नए वार्तालाप के लिए साइड कार्ड की विंडो चौड़ाई का डिफ़ॉल्ट हिस्सा (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'चैट फ़ाइलें साइडबार में खोलें',
settingsOpenPathDesc: 'चैट में फ़ाइल लिंक (टूल पंक्ति, उत्पादित फ़ाइलें, उल्लेख) क्लिक करने पर सिस्टम डिफ़ॉल्ट ऐप के बजाय साइडबार एडिटर में खोलें',
settingsOpenToolsTitle: 'मॉडल के लिए साइडबार ओपन टूल इंजेक्ट करें',
settingsOpenToolsDesc: 'चालू होने पर, मॉडल sidebar_open टूल से साइडबार में फ़ाइलें, फ़ोल्डर और HTTP(S) पेज खोल सकता है (डिफ़ॉल्ट रूप से बंद)',
settingsTitleBarTitle: 'स्थिति संगतता मोड',
-2
View File
@@ -235,8 +235,6 @@ export const id: Record<string, string> = {
settingsWidthTitle: 'Porsi lebar default',
settingsWidthDesc: 'Porsi lebar jendela default kartu samping untuk obrolan baru (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Buka berkas obrolan di sidebar',
settingsOpenPathDesc: 'Buka tautan berkas di obrolan (baris alat, berkas yang dihasilkan, mention) di editor sidebar alih-alih aplikasi default sistem',
settingsOpenToolsTitle: 'Suntik alat buka sidebar untuk model',
settingsOpenToolsDesc: 'Saat diaktifkan, model dapat membuka file, folder, dan halaman HTTP(S) di sidebar melalui alat sidebar_open (nonaktif secara default)',
settingsTitleBarTitle: 'Mode kompatibilitas posisi',
-2
View File
@@ -228,8 +228,6 @@ export const it: Record<string, string> = {
settingsWidthTitle: 'Quota di larghezza predefinita',
settingsWidthDesc: 'La quota predefinita della scheda laterale sulla larghezza della finestra per le nuove conversazioni (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Apri i file della chat nella barra laterale',
settingsOpenPathDesc: 'Apre i collegamenti ai file nella chat (righe di strumenti, file prodotti, menzioni) nell’editor della barra laterale invece dell’app predefinita di sistema',
settingsOpenToolsTitle: 'Iniettare lo strumento di apertura della barra laterale per il modello',
settingsOpenToolsDesc: 'Se attivato, il modello può aprire file, cartelle e pagine HTTP(S) nella barra laterale tramite lo strumento sidebar_open (disattivato per impostazione predefinita)',
settingsTitleBarTitle: 'Modalità di compatibilità della posizione',
-2
View File
@@ -237,8 +237,6 @@ export const ja: Record<string, string> = {
settingsWidthTitle: 'デフォルト幅の比率',
settingsWidthDesc: '新規会話時のサイドカードがウィンドウ幅に占める割合 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'チャット内ファイルをサイドバーで開く',
settingsOpenPathDesc: 'チャット内のファイルリンク(ツール行、産物リスト、ファイル言及)クリック時に、システム既定アプリではなくサイドバーエディターで開く',
settingsOpenToolsTitle: 'モデルにサイドバー開くツールを注入',
settingsOpenToolsDesc: 'オンにすると、モデルは sidebar_open ツールでサイドバーにファイル・フォルダー・HTTP(S) ページを開ける(デフォルトオフ)',
settingsTitleBarTitle: '位置互換モード',
-2
View File
@@ -229,8 +229,6 @@ export const ko: Record<string, string> = {
settingsWidthTitle: '기본 너비 비율',
settingsWidthDesc: '새 대화에서 사이드 카드가 창 너비에서 차지하는 비율(%) (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '채팅 영역 파일을 사이드바에서 열기',
settingsOpenPathDesc: '채팅에서 파일 링크(도구 행, 산출물 목록, 파일 언급)를 클릭하면 사이드바 편집기에서 열고, 시스템 기본 앱은 호출하지 않습니다',
settingsOpenToolsTitle: '모델에 사이드바 열기 도구 주입',
settingsOpenToolsDesc: '켜면 모델이 sidebar_open 도구로 사이드바에서 파일·폴더·HTTP(S) 페이지를 열 수 있습니다(기본 꺼짐)',
settingsTitleBarTitle: '위치 호환 모드',
-2
View File
@@ -235,8 +235,6 @@ export const nl: Record<string, string> = {
settingsWidthTitle: 'Standaard breedte-aandeel',
settingsWidthDesc: 'Het standaard breedte-aandeel van de zijkaart voor nieuwe conversaties (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Chatbestanden in de zijbalk openen',
settingsOpenPathDesc: 'Bestandslinks in de chat (toolrijen, geproduceerde bestanden, vermeldingen) openen in de zijbalk-editor in plaats van de standaard systeemapp',
settingsOpenToolsTitle: 'Zijbalk-openen-tool voor het model injecteren',
settingsOpenToolsDesc: 'Indien ingeschakeld kan het model bestanden, mappen en HTTP(S)-pagina\'s in de zijbalk openen via de sidebar_open-tool (standaard uit)',
settingsTitleBarTitle: 'Positiecompatibiliteitsmodus',
-2
View File
@@ -239,8 +239,6 @@ export const pl: Record<string, string> = {
settingsWidthTitle: 'Domyślny udział szerokości',
settingsWidthDesc: 'Domyślny udział szerokości okna dla karty bocznej w nowych rozmowach (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Otwieraj pliki czatu w panelu bocznym',
settingsOpenPathDesc: 'Otwieraj linki plików na czacie (wiersze narzędzi, wyprodukowane pliki, wzmianki) w edytorze panelu bocznego zamiast w domyślnej aplikacji systemu',
settingsOpenToolsTitle: 'Wstrzyknij narzędzie otwierania panelu bocznego dla modelu',
settingsOpenToolsDesc: 'Po włączeniu model może otwierać pliki, foldery i strony HTTP(S) w panelu bocznym za pomocą narzędzia sidebar_open (domyślnie wyłączone)',
settingsTitleBarTitle: 'Tryb zgodności pozycji',
-2
View File
@@ -220,8 +220,6 @@ export const pt: Record<string, string> = {
settingsWidthTitle: 'Participação padrão na largura',
settingsWidthDesc: 'Participação padrão do cartão lateral na largura da janela em novas conversas (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Abrir arquivos do chat na barra lateral',
settingsOpenPathDesc: 'Abrir links de arquivos no chat (linhas de ferramentas, arquivos produzidos, menções) no editor da barra lateral em vez do aplicativo padrão do sistema',
settingsOpenToolsTitle: 'Injetar a ferramenta de abertura na barra lateral para o modelo',
settingsOpenToolsDesc: 'Quando ativado, o modelo pode abrir arquivos, pastas e páginas HTTP(S) na barra lateral pela ferramenta sidebar_open (desativado por padrão)',
settingsTitleBarTitle: 'Modo de compatibilidade de posição',
-2
View File
@@ -234,8 +234,6 @@ export const ru: Record<string, string> = {
settingsWidthTitle: 'Доля ширины по умолчанию',
settingsWidthDesc: 'Доля боковой карточки в ширине окна для новых сессий, в процентах (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Открывать файлы чата в боковой панели',
settingsOpenPathDesc: 'При щелчке по ссылке на файл в чате (строки инструментов, списки результатов, упоминания) файл открывается в редакторе боковой панели, а не в системном приложении по умолчанию',
settingsOpenToolsTitle: 'Внедрить инструмент открытия в боковой панели для модели',
settingsOpenToolsDesc: 'Если включено, модель может открывать файлы, папки и HTTP(S)-страницы в боковой панели с помощью инструмента sidebar_open (по умолчанию выключено)',
settingsTitleBarTitle: 'Режим совместимости заголовка',
-2
View File
@@ -220,8 +220,6 @@ export const sv: Record<string, string> = {
settingsWidthTitle: 'Standardbreddandel',
settingsWidthDesc: 'Sidokortets standardandel av fönsterbredden för nya konversationer (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Öppna chattfiler i sidopanelen',
settingsOpenPathDesc: 'Öppna fillänkar i chatten (verktygsrader, producerade filer, omnämnanden) i sidopanelens editor i stället för systemets standardapp',
settingsOpenToolsTitle: 'Injicera sidopanel-öppningsverktyg för modellen',
settingsOpenToolsDesc: 'När aktiverat kan modellen öppna filer, mappar och HTTP(S)-sidor i sidopanelen via sidebar_open-verktyget (av som standard)',
settingsTitleBarTitle: 'Positions kompatibilitetsläge',
-2
View File
@@ -237,8 +237,6 @@ export const th: Record<string, string> = {
settingsWidthTitle: 'สัดส่วนความกว้างเริ่มต้น',
settingsWidthDesc: 'สัดส่วนความกว้างเริ่มต้นของการ์ดด้านข้างจากความกว้างหน้าต่างสำหรับแชทใหม่ (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'เปิดไฟล์แชทในแถบด้านข้าง',
settingsOpenPathDesc: 'เปิดลิงก์ไฟล์ในแชท (แถวเครื่องมือ, ไฟล์ที่สร้าง, การกล่าวถึง) ในตัวแก้ไขแถบด้านข้างแทนแอปเริ่มต้นของระบบ',
settingsOpenToolsTitle: 'ฉีดเครื่องมือเปิดแถบด้านข้างสำหรับโมเดล',
settingsOpenToolsDesc: 'เมื่อเปิดใช้ โมเดลสามารถเปิดไฟล์ โฟลเดอร์ และหน้า HTTP(S) ในแถบด้านข้างผ่านเครื่องมือ sidebar_open (ปิดเป็นค่าเริ่มต้น)',
settingsTitleBarTitle: 'โหมดความเข้ากันได้ของตำแหน่ง',
-2
View File
@@ -237,8 +237,6 @@ export const tr: Record<string, string> = {
settingsWidthTitle: 'Varsayılan genişlik payı',
settingsWidthDesc: 'Yeni sohbetler için yan kartın pencere genişliğindeki varsayılan payı (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Sohbet dosyalarını kenar çubuğunda aç',
settingsOpenPathDesc: 'Sohbetteki dosya bağlantılarını (araç satırları, üretilen dosyalar, anılmalar) sistem varsayılan uygulaması yerine kenar çubuğu düzenleyicisinde aç',
settingsOpenToolsTitle: 'Model için kenar çubuğu açma aracı enjekte et',
settingsOpenToolsDesc: 'Etkinleştirildiğinde model, sidebar_open aracıyla kenar çubuğunda dosyaları, klasörleri ve HTTP(S) sayfalarını açabilir (varsayılan kapalı)',
settingsTitleBarTitle: 'Konum uyumluluk modu',
-2
View File
@@ -237,8 +237,6 @@ export const vi: Record<string, string> = {
settingsWidthTitle: 'Tỷ lệ chiều rộng mặc định',
settingsWidthDesc: 'Tỷ lệ phần trăm chiều rộng cửa sổ mà thẻ bên chiếm cho phiên mới (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Mở tệp chat trong thanh bên',
settingsOpenPathDesc: 'Khi nhấp link tệp trong chat (dòng công cụ, danh sách sản phẩm, nhắc tệp), mở trong trình soạn thảo thanh bên thay vì ứng dụng mặc định hệ thống',
settingsOpenToolsTitle: 'Tiêm công cụ mở thanh bên cho mô hình',
settingsOpenToolsDesc: 'Khi bật, mô hình có thể mở tệp, thư mục và trang HTTP(S) trong thanh bên qua công cụ sidebar_open (mặc định tắt)',
settingsTitleBarTitle: 'Chế độ tương thích thanh tiêu đề',
-2
View File
@@ -252,8 +252,6 @@ export const zhHK: Record<string, string> = {
settingsWidthTitle: '預設寬度佔比',
settingsWidthDesc: '新增工作階段時側邊卡片佔視窗寬度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天區檔案在側邊欄開啟',
settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式',
settingsOpenToolsTitle: '為模型注入側邊欄開啟工具',
settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)',
settingsTitleBarTitle: '位置相容模式',
-2
View File
@@ -252,8 +252,6 @@ export const zhMO: Record<string, string> = {
settingsWidthTitle: '預設寬度佔比',
settingsWidthDesc: '新增工作階段時側邊卡片佔視窗寬度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天區檔案在側邊欄開啟',
settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式',
settingsOpenToolsTitle: '為模型注入側邊欄開啟工具',
settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)',
settingsTitleBarTitle: '位置相容模式',
-2
View File
@@ -252,8 +252,6 @@ export const zhTW: Record<string, string> = {
settingsWidthTitle: '預設寬度佔比',
settingsWidthDesc: '新增工作階段時側邊卡片佔視窗寬度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天區檔案在側邊欄開啟',
settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式',
settingsOpenToolsTitle: '為模型注入側邊欄開啟工具',
settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)',
settingsTitleBarTitle: '位置相容模式',
-4
View File
@@ -249,8 +249,6 @@ export const zh = {
settingsWidthTitle: '默认宽度占比',
settingsWidthDesc: '新建会话时侧边卡片占窗口宽度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天区文件在侧边栏打开',
settingsOpenPathDesc: '在聊天里点击文件链接(工具行、产物列表、文件提及)时,在侧边栏编辑器中打开,不再调用系统默认应用',
settingsOpenToolsTitle: '为模型注入侧边栏打开工具',
settingsOpenToolsDesc: '开启后,模型可通过 sidebar_open 工具在侧边栏主动打开文件、文件夹和 HTTP(S) 网页(默认关闭)',
settingsTitleBarTitle: '位置兼容模式',
@@ -699,8 +697,6 @@ export const en: Record<keyof typeof zh, string> = {
settingsWidthTitle: 'Default width share',
settingsWidthDesc: 'The side card\'s default share of the window width for new conversations (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Open chat files in the sidebar',
settingsOpenPathDesc: 'Open file links in the chat (tool rows, produced files, mentions) in the sidebar editor instead of the system default app',
settingsOpenToolsTitle: 'Inject the sidebar-open tool for the model',
settingsOpenToolsDesc: 'When enabled, the model can actively open files, folders, and HTTP(S) pages in the sidebar through the sidebar_open tool (off by default)',
settingsTitleBarTitle: 'Position compatibility mode',
-144
View File
@@ -1,144 +0,0 @@
/**
* Interception of the chat's file-open funnel (DSH 0.1.2-alpha hosts).
*
* The alpha funnel is the Typert remote namespace `remote.session`: every
* chat-side file open (tool-row path links, the produced-files row, prose
* file mentions, inline-code paths) funnels through ChatView's injected
* `openFile`, which calls `ctx.remote.session.openWorkspacePath({ path })`
* with the path ALREADY resolved against the session cwd (verified against
* the DSH source: `packages/client/ui-chat/src/client/apply.ts` is the only
* production caller). The host side (`api/session-controller`) hands the
* path to the OS's default application (`openNativePath` → xdg-open on
* Linux). The pre-alpha funnel `ctx.workspaces.openPath` is gone — the
* alpha `IWorkspaces` has no openPath at all.
*
* `remote.session` is a cordis service (the gateway client's
* RemoteNamespaceService, key `remote.session`) whose methods are installed
* as ACCESSOR properties (`Object.defineProperty(service, method, {
* configurable: true, get })`) — the getter reads the method table at
* ACCESS time and returns the invocation closure. A plain assignment would
* not stick (no setter), so the wrapper shadows the method with a data
* property and restores the captured accessor descriptor on dispose.
*
* The namespace service appears asynchronously (the session-controller
* contribution mounts after the connection is up) and is disposed/recreated
* on contribution remounts; the caller therefore enters through
* `ctx.inject(['remote.session'], …)`, which re-fires on every remount.
*
* The wrapper is dependency-free by design (no React / ui-primitives), so
* the takeover logic is unit-testable and the file stays importable from
* the test runtime.
*/
/** The one request shape the funnel method takes (mirror of the host's
* SessionOpenWorkspacePathRequest). */
export interface OpenWorkspacePathRequest {
/** Absolute path (the chat caller already resolved it against the cwd). */
path: string
}
/**
* What the funnel method resolves to (mirror of the typert `RemoteResult`
* envelope): EVERY remote method folds its business value into
* `{ ok: true, value }` / `{ ok: false, error }` — the chat caller branches
* on `result.ok`, so a bare business value would be misread as a failure
* (`result.error.message` reads `message` off undefined).
*/
export type OpenWorkspacePathResult =
| { readonly ok: true; readonly value: { opened: boolean } }
| { readonly ok: false; readonly error: { readonly code: string; readonly message: string; readonly details: object } }
/** The namespace slice the wrapper shadows (mirror of the gateway client's
* RemoteNamespaceService for the `session` namespace). */
export interface OpenWorkspacePathService {
openWorkspacePath(request: OpenWorkspacePathRequest, signal?: AbortSignal): Promise<OpenWorkspacePathResult>
}
/** Per-call decisions the wrapper needs (wired to the store + ctx in the client half). */
export interface OpenPathInterceptDeps {
/**
* Whether to take over this call: the `interceptOpenPath` pref AND the
* editor tab's own enable switch must both be on (an editor that cannot
* open must not swallow opens — they fall through to the Host).
*/
takeoverEnabled(): boolean
/** The session whose scope the sidebar editor loads the file in (current session). */
currentSessionId(): string | undefined
/** Route the open into the sidebar editor (the established openSidebarFile). */
openInSidebar(path: string, sessionId: string): void
/** Route a folder-reveal gesture ("Show in folder" passes '.') into the sidebar explorer. */
revealInExplorer(path: string, sessionId: string): void
}
/**
* Whether a path is the "Show in folder" folder-reveal gesture. The stock
* ui-deliverables row passes `'.'` (the session workspace root, resolved by
* the chat view to `"<cwd>/."`); any path whose final segment is `.` is the
* same gesture. A directory has no editor content, so these opens must reach
* the explorer instead of an editor tab.
*/
export function isFolderRevealPath(path: string): boolean {
if (path === '.' || path === './') return true
const trimmed = path.replace(/[\\/]+$/, '')
return trimmed === '.' || /[\\/]\.$/.test(trimmed)
}
/**
* Shadow `remote.session.openWorkspacePath`: intercepted calls open the file
* in the sidebar editor and resolve with the remote SUCCESS ENVELOPE
* (`{ ok: true, value: { opened: true } }`, so ChatView shows no error
* dialog); anything that declines falls through to the captured original
* closure (the host OS's default application) untouched.
* The one exception is the folder-reveal gesture, which is routed to
* {@link OpenPathInterceptDeps.revealInExplorer} instead.
*
* The original closure is captured by ACCESSING the accessor once at wrap
* time — it invokes whatever method records are mounted at that moment. If
* the contribution remounts its methods while the shadow is installed, the
* captured closure points at the old records; the session namespace is
* effectively permanent in practice, so this is accepted and the shadow is
* re-applied anyway when the remount recreates the service (the caller's
* `ctx.inject` re-fires).
*
* @param service - the `remote.session` namespace service.
* @param deps - per-call takeover decisions.
* @returns the disposer restoring the original accessor descriptor (HMR-safe).
*/
export function wrapOpenWorkspacePath(
service: OpenWorkspacePathService,
deps: OpenPathInterceptDeps,
): () => void {
const KEY = 'openWorkspacePath'
const target = service as object
const descriptor = Object.getOwnPropertyDescriptor(target, KEY)
// Invoke the accessor once to capture the CURRENT invocation closure.
const original = service.openWorkspacePath
// The gateway installs the namespace's whole method group in the same
// synchronous window as the service registration, so an inject-observed
// service always has the method — but a hand-rolled composition might not;
// wrapping nothing would turn fall-throughs into crashes, so decline.
if (typeof original !== 'function') return () => {}
const wrapped = (request: OpenWorkspacePathRequest, signal?: AbortSignal): Promise<OpenWorkspacePathResult> => {
if (deps.takeoverEnabled()) {
const sessionId = deps.currentSessionId()
if (sessionId !== undefined) {
if (isFolderRevealPath(request.path)) deps.revealInExplorer(request.path, sessionId)
else deps.openInSidebar(request.path, sessionId)
return Promise.resolve({ ok: true, value: { opened: true } })
}
}
return original.call(service, request, signal)
}
Object.defineProperty(target, KEY, {
configurable: true,
enumerable: true,
writable: true,
value: wrapped,
})
return () => {
// Restore the exact original property (the gateway's accessor) so a
// chain of wrappers keeps working across disposals in any order.
if (descriptor !== undefined) Object.defineProperty(target, KEY, descriptor)
else Reflect.deleteProperty(target, KEY)
}
}
-3
View File
@@ -76,9 +76,6 @@ export function parsePrefs(value: unknown): SidebarPrefs {
terminalFontSize: typeof record.terminalFontSize === 'number' && Number.isFinite(record.terminalFontSize)
? clampTerminalFontSize(record.terminalFontSize)
: SIDEBAR_PREFS_DEFAULTS.terminalFontSize,
interceptOpenPath: typeof record.interceptOpenPath === 'boolean'
? record.interceptOpenPath
: SIDEBAR_PREFS_DEFAULTS.interceptOpenPath,
editorExplorer: typeof record.editorExplorer === 'boolean'
? record.editorExplorer
: SIDEBAR_PREFS_DEFAULTS.editorExplorer,
+1 -1
View File
@@ -465,7 +465,7 @@ export function matchUrlTarget(tabs: readonly TabDescriptor[], url: URL): TabDes
* The plugin version this service instance reports. Keep in lockstep with
* `package.json`'s version — `tests/service.spec.ts` asserts the pair.
*/
export const SIDEBAR_SERVICE_VERSION = '0.18.1'
export const SIDEBAR_SERVICE_VERSION = '0.19.0-alpha.0'
/**
* Monotonic capability list consumers use to gate new API usage (features
+8 -6
View File
@@ -13,9 +13,10 @@
* shows only the thread's own conversation.
*
* Live streaming: `assistant/message` events only land when a step
* completes, but `assistant/chunk` events stream token-level text and
* reasoning deltas. The mapping accumulates both per block and supersedes
* them with the assembled message once it lands (settled rows).
* completes, but the host's `assistant/live-chunk` rows stream token-level
* text and reasoning deltas (DSH 0.1.5 publishes them outside the session
* log — see assistant-live.ts). The mapping accumulates both per block and
* supersedes them with the assembled message once it lands (settled rows).
*/
import type { DiffHunk, ReadBlockLine } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SidebarHistoryEntry } from '../context-types.ts'
@@ -299,12 +300,13 @@ function resultCard(
/**
* Map a thread child's history rows onto compact transcript rows: the
* inherited fork seed is cut at the last `session/end-seed`, context
* injections map onto a collapsible injection row, `assistant/chunk`
* injections map onto a collapsible injection row, `assistant/live-chunk`
* deltas accumulate into streaming rows per (turn, step, block) and are
* superseded by the assembled `assistant/message`, and tool invocations
* render one expandable line each (arguments, paired result text, failure
* marker; a still-executing call is marked until its result lands).
* @param entries - history rows (event + host-computed view) in seq order.
* @param entries - history rows (event + host-computed view) in seq order,
* the live deltas of the in-flight attempt appended after the durable tail.
* @returns display rows in log order.
*/
export function transcriptRows(entries: readonly SidebarHistoryEntry[], prev?: readonly SidechatTranscriptRow[]): SidechatTranscriptRow[] {
@@ -374,7 +376,7 @@ export function transcriptRows(entries: readonly SidebarHistoryEntry[], prev?: r
rows.push({ kind: 'user', seq: event.seq, text })
break
}
case 'assistant/chunk': {
case 'assistant/live-chunk': {
const chunk = data.chunk as { type?: unknown; text?: unknown } | undefined
if (chunk === null || typeof chunk !== 'object') break
const kind = chunk.type === 'text-delta' ? 'assistant' : chunk.type === 'reasoning-delta' ? 'reasoning' : null