mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-03 06:18:22 +00:00
# Conflicts: # .agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.i18n.yaml # .agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md # .agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md # .agents/notes/implemented/feature/2026-07-21-cross-session-references.i18n.yaml # .agents/notes/implemented/feature/2026-07-21-cross-session-references.md # .agents/notes/implemented/feature/2026-07-21-cross-session-references.zh.md # .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml # apps/cli/config/web.cordis.yml # apps/cli/package.json # apps/web/tests/scaffold.ts # docs/capability-seams.md # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/event-producer-consumer.md # docs/module-graph.md # packages/client/connection/src/client/api.ts # packages/client/connection/src/client/fixture.ts # packages/client/connection/src/client/index.ts # packages/client/runtime/src/client/contract/session.ts # packages/client/runtime/src/client/sessions/session.ts # packages/client/runtime/tests/session.client.spec.ts # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/README.md # packages/client/ui-conversation/README.zh.md # packages/client/ui-conversation/src/client/chat/ChatView.tsx # packages/client/ui-conversation/src/client/chat/MessageItem.tsx # packages/client/ui-conversation/src/client/chat/chat-flow.ts # packages/client/ui-conversation/src/client/input/facade.ts # packages/client/ui-conversation/src/client/input/hub.ts # packages/client/ui-conversation/tests/apply-inject.client.spec.tsx # packages/client/ui-conversation/tests/chat-branch-tails.client.spec.tsx # packages/client/ui-conversation/tests/chat-view.client.spec.tsx # packages/client/ui-conversation/tests/input-bar.client.spec.tsx # packages/client/ui-conversation/tests/input-matrix.client.spec.tsx # packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx # packages/client/ui-conversation/tests/skeleton.client.spec.tsx # packages/client/ui-input-trigger/package.json # packages/client/ui-input-trigger/src/types.ts # packages/client/ui-jobs/README.i18n.yaml # packages/client/ui-slash/README.md # packages/client/ui-slash/README.zh.md # packages/client/ui-subagent/README.i18n.yaml # packages/client/ui-subagent/README.md # packages/client/ui-subagent/README.zh.md # packages/client/ui-subagent/package.json # packages/client/ui-subagent/src/client/index.ts # packages/client/ui-subagent/tests/browser-plugin.client.spec.ts # packages/client/ui-subagent/tsconfig.json # packages/context/session-reference/README.i18n.yaml # packages/context/session-reference/README.md # packages/context/session-reference/README.zh.md # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/session/README.i18n.yaml # packages/core/session/README.zh.md # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/index.ts # packages/host/apiproxy/src/api/rpc-map.ts # packages/host/apiproxy/src/api/rpc.schema.ts # packages/host/apiproxy/src/api/rpc.ts # packages/host/apiproxy/src/api/sessions.ts # packages/host/apiproxy/src/fetch/client.ts # packages/host/apiproxy/src/fetch/handler.ts # packages/host/apiproxy/src/index.ts # packages/host/apiproxy/tests/client-handler.spec.ts # packages/host/apiproxy/tsconfig.json # pnpm-lock.yaml # scripts/gen-cordis-catalog.ts # scripts/gen-doc-graphs.ts # scripts/verify-package-readme-model-experience.ts # tsconfig.base.json # tsconfig.client.json # vitest.config.ts
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
/**
|
|
* Trigger detection pure core. Scans backward from
|
|
* the caret for a live trigger char under the guard tier and applies the
|
|
* word-boundary rules. Zero React / DOM / cordis.
|
|
*/
|
|
import { activeAtToken } from '@deepseek-ai/dsh-file-reference/grammar'
|
|
import type { TriggerChar } from '../types.ts'
|
|
import type { DetectTrigger } from './contract.ts'
|
|
|
|
const WORD_CHAR = /[\p{L}\p{N}_]/u
|
|
const WHITESPACE = /\s/u
|
|
|
|
/**
|
|
* Word-boundary rule: a trigger char opens only at start-of-draft, after
|
|
* whitespace (newlines included), or after punctuation. Two URL carve-outs
|
|
* keep '/' dead inside URLs (both pinned by tests): '/' after a ':' that
|
|
* itself follows a non-whitespace char (scheme separator, `https:/…`), and
|
|
* '/' directly after another '/' (second slash of `//`).
|
|
*/
|
|
function boundaryOk(draft: string, index: number, char: TriggerChar): boolean {
|
|
if (index === 0) return true
|
|
const prev = draft.charAt(index - 1)
|
|
if (WHITESPACE.test(prev)) return true
|
|
if (WORD_CHAR.test(prev)) return false
|
|
if (char === '/') {
|
|
if (prev === '/') return false
|
|
if (prev === ':' && index >= 2 && !WHITESPACE.test(draft.charAt(index - 2))) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Detect a trigger token at the caret. `@` first uses the shared grammar,
|
|
* including an open quoted token that may span whitespace. Slash detection
|
|
* scans left to the first whitespace; slashes failing the word boundary are
|
|
* treated as ordinary token chars and the scan continues (URL slashes).
|
|
* Guard tiers: plain = both chars live; claimed = '/' fully suppressed,
|
|
* '@' live; frozen = none.
|
|
*
|
|
* @param draft - Full draft text.
|
|
* @param caret - Caret offset into `draft`.
|
|
* @param guard - Availability tier derived from the input phase.
|
|
* @returns The hit with `query` = trigger-to-caret slice and `span` =
|
|
* `{start: triggerIndex, end: caret}`; `span.draftRev` is a placeholder `0`
|
|
* — the calling shell stamps the real revision. Null when no trigger is
|
|
* live at the caret.
|
|
*/
|
|
export const detectTrigger: DetectTrigger = (draft, caret, guard) => {
|
|
if (guard.tier === 'frozen') return null
|
|
const at = activeAtToken(draft, caret)
|
|
if (at !== undefined) {
|
|
const start = caret - at.prefix.length
|
|
return {
|
|
trigger: '@',
|
|
query: at.query,
|
|
quoted: at.quoted,
|
|
position: draft.search(/\S/) === start ? 'leading' : 'inline',
|
|
span: { start, end: caret, draftRev: 0 },
|
|
}
|
|
}
|
|
for (let i = caret - 1; i >= 0; i--) {
|
|
const ch = draft.charAt(i)
|
|
if (WHITESPACE.test(ch)) return null
|
|
if (ch !== '/') continue
|
|
if (guard.tier === 'claimed') continue
|
|
if (!boundaryOk(draft, i, ch)) continue
|
|
return {
|
|
trigger: ch,
|
|
query: draft.slice(i + 1, caret),
|
|
quoted: false,
|
|
position: draft.search(/\S/) === i ? 'leading' : 'inline',
|
|
span: { start: i, end: caret, draftRev: 0 },
|
|
}
|
|
}
|
|
return null
|
|
}
|