@sidequest-007/dsh-atlas 1.0.1 → 1.0.2

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 的组织方式,版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [1.0.2] - 2026-09-27
6
+
7
+ ### 修复
8
+
9
+ - **带空格的会话引用不再被切成半个 token**:草稿 token 的读取边界是"到空白为止",而会话标签里经常有空格
10
+ (`@[继续 LoongCrush J 项目的任务](dsh-session:…)`),于是它被切成 `@[继续` 这一片。现在完整的 wire 引用
11
+ (方括号 + `dsh-session:` 载荷)**优先**被作为一个 token 读入;官方引用语法本来就只有这一种承载空格标签的
12
+ 写法,所以修复后这类引用能正确解码成会话并画出可点链接(而不是落进文件规则)。
13
+ - **诊断线索**:手打**裸标签**式(`@继续 LoongCrush J 项目的任务`,没有方括号与载荷)在语义上不是会话引用,
14
+ token 只能是 `@继续`;这类不存在的路径在验证阶段会让 Host 报
15
+ `cannot resolve target "…\继续": ENOENT`。要引用会话请用 `@` 菜单的「过去的聊天」插入,它会写入带载荷的
16
+ 方括号形态。
17
+
18
+ ### 新增
19
+
20
+ - **会话引用可点击跳转**:草稿里手打的 `@<会话id>`(或 `@[标题](dsh-session:…)`)与已发送气泡里仍带 wire 形态的会话引用,
21
+ 现在都是**可点链接**——点击即切换当前会话(复用侧栏列表项的同一动作,外观/路由/持久化一致),跨工作区有效。
22
+ 判定规则刻意保守:wire 形态按 payload 精确解码(base64url + 规范化回环校验,坏 URI 一律拒绝);
23
+ **裸标签只有唯一命中**(等于某个会话 id,或等于**唯一一个**会话标题)才成为会话链接,
24
+ 所以 `@AGENTS.md` 这种同时也是工作区文件的名字**永远不会被劫持**;同名/过期会话保持原样(可读、不可点)。
25
+ **边界**:**已发送**的气泡只有在 chip 仍带 `dsh-session:` 载荷时才可点 —— Host 在落库前会把提及折成裸标签,
26
+ 那时 id 已不在文本里(已按上游通道提报)。
27
+ - **新增回归守卫**:补上"profile 未挂 right Sidebar"这条已文档化降级路径的测试(此前**零覆盖**),以及
28
+ 会话引用的**组合层接线**守卫;并用"故意破坏实现"实验证明这些守卫真的会失败。
29
+
30
+ ### 文档
31
+
32
+ - **介绍口径修正**:tagline 与首段补上两点此前只写在代码里的事实 —— ① **会话引用本身就是链接**(草稿里点它即切换会话);
33
+ ② 本部署实际是 **6 个资源族**(五个内置类别 + 内置的 `@git` provider)。此前的措辞只说"五个内置类别 + provider",
34
+ 读者不会知道菜单里真能看到并使用 `@git`。同步改了 `package.json` 的 `description`(npm 卡片用)、配图的
35
+ `aria-label` 与图内文案,以及"输入快捷键只显示五个类别"这类描述。
36
+
5
37
  ## [1.0.1] - 2026-09-26
6
38
 
7
39
  ### 变更
package/README.md CHANGED
@@ -4,17 +4,17 @@
4
4
 
5
5
  A **Side Quest** project (工作室:支线任务, **Side Quest Labs**) — npm scope `@sidequest-007/*`, repository [`jameswatt139240-crypto/dsh-ATLAS`](https://github.com/jameswatt139240-crypto/dsh-ATLAS); the DSH plugin id keeps the ecosystem's unscoped `dsh-*` form.
6
6
 
7
- <img src="assets/diagrams/atlas-overview.svg" alt="dsh-ATLAS: one @ trigger, five built-in categories, plus a category any plugin can register" width="880">
7
+ <img src="assets/diagrams/atlas-overview.svg" alt="dsh-ATLAS: one @ trigger, five built-in categories plus the bundled git category, and a category any plugin can register" width="880">
8
8
 
9
- **@ Last, All Sources.** One `@` . Any plugin can register.
9
+ **@ Last, All Sources.** One `@`, every source; one click, the session it names.
10
10
 
11
- Unified `@` mentions for the DeepSeek Harness web GUI: type `@` in the composer to reference **workspace files and folders**, **discoverable skills**, **past chats**, and **installed plugins** — and any other plugin can add its own category to the same menu.
11
+ Unified `@` mentions for the DeepSeek Harness web GUI: type `@` in the composer to reference **workspace files and folders**, **discoverable skills**, **past chats**, **installed plugins**, and **workspace git changes** — and any other plugin can add its own category to the same menu. A **session** reference is a link: click it and the composer switches to that session.
12
12
 
13
- - **One `@`, every source**: five built-in categories plus every registered provider, in one list; typing letters searches all of them at once.
13
+ - **One `@`, every source**: five built-in categories plus the bundled `@git` provider — six resource families in this deployment — and every registered provider, in one list; typing letters searches all of them at once.
14
14
  - **A platform, not a picker**: third-party plugins register their own `@` category through the `ctx.atlas` seam. The menu rebuilds from the live registry on every open, so **this package never has to change for a provider** — see [Write your own `@` source](#write-your-own--source).
15
15
  - **Provenance, never content**: a committed mention injects a marker (`<workspace-reference>`, `<skill-reference>`, `<atlas-reference>`, …) and nothing else. The plugin reads directory metadata only; the agent reads the file itself, if and when it needs to.
16
16
  - **Governed by construction**: `scopes` and `testedOn` are required, a duplicate `id` throws, a version mismatch registers as `verified: false`, and every provider body is budgeted (16 KiB per reference, 48 KiB per step).
17
- - **`@git` ships as the worked example**: a real provider, built the way a third party would build one, in ~200 lines across its two halves.
17
+ - **`@git` ships as the worked example, and as a real source**: it lists the workspace's changes (path + added/removed lines) like any other category, and it is built exactly the way a third party would build one, in ~200 lines across its two halves.
18
18
 
19
19
  <p align="center"><img src="assets/diagrams/atlas-seam.svg" alt="The @ data-source seam: browser half lists candidates, host half resolves one reference at send, and the registry gates the declaration" width="880"></p>
20
20
 
@@ -40,9 +40,9 @@ dsh plugin --profile web add @sidequest-007/dsh-atlas
40
40
 
41
41
  Restart `dsh web` after installing or updating so the Host and the browser client load the same version. Then type `@` in the composer.
42
42
 
43
- ## The five categories
43
+ ## The categories
44
44
 
45
- The menu keeps five category rows in its **bottom band** — the rows nearest the composer — so pressing `@` opens on them rather than on whatever else shares the trigger.
45
+ The menu keeps the category rows — five built in, plus the bundled `@git` — in its **bottom band**, the rows nearest the composer, so pressing `@` opens on them rather than on whatever else shares the trigger.
46
46
 
47
47
  | Category | Menu prefix | Shortcut | Candidates from | Written into the draft |
48
48
  |---|---|---|---|---|
@@ -52,9 +52,9 @@ The menu keeps five category rows in its **bottom band** — the rows nearest th
52
52
  | Past chat | `chat:` | `C` | The official session-reference resolver | `@[title](dsh-session:<id>)` |
53
53
  | Plugin | `plugin:` | `P` | The official plugin inventory | `@plugin:dsh-atlas` |
54
54
 
55
- **One shortcut letter is a statement of intent, not a query.** Typing it shows the five categories plus a `Tab 补全 → category:` hint as the **last** row, with the highlight already on that hint; <kbd>Tab</kbd> or <kbd>Enter</kbd> then enters the `category:` prefix. **A registered source can claim a shortcut letter too**: the first letter of its id (`@git` → <kbd>G</kbd>), and only while no built-in category or earlier provider answers to that letter (the `@<id>:` prefix always works). Filtering starts on anything more specific — a first letter that is not a shortcut, or a second character (which is what the `fi`/`fo`/`sk`/`ch`/`pl` spellings are). Clicking a category row or the back row enters/returns the same way and **keeps the menu open** — that is this plugin's own override; the normal pick path closes it.
55
+ **One shortcut letter is a statement of intent, not a query.** Typing it shows the category rows — five built-in plus the bundled `@git` — and a `Tab 补全 → category:` hint as the **last** row, with the highlight already on that hint; <kbd>Tab</kbd> or <kbd>Enter</kbd> then enters the `category:` prefix. **A registered source can claim a shortcut letter too**: the first letter of its id (`@git` → <kbd>G</kbd>), and only while no built-in category or earlier provider answers to that letter (the `@<id>:` prefix always works). Filtering starts on anything more specific — a first letter that is not a shortcut, or a second character (which is what the `fi`/`fo`/`sk`/`ch`/`pl` spellings are). Clicking a category row or the back row enters/returns the same way and **keeps the menu open** — that is this plugin's own override; the normal pick path closes it.
56
56
 
57
- **Registered sources are the sixth row and beyond.** `@git` ships built in (the workspace's changed files, with each file's diff as the referenced content), and any plugin can add its own category the same way — see [Write your own `@` source](#write-your-own--source). They appear after the five, in registration order, and disappear when the plugin that registered them is disposed. The plugin's group itself is registered with a **positive order**, so it is the last group in the menu: the bottom band belongs to this plugin, and the stock `dsh-client-ui-reference` source's file list stays above it.
57
+ **Registered sources are the sixth row and beyond.** `@git` ships built in (the workspace's changed files, with each file's diff as the referenced content), and any plugin can add its own category the same way — see [Write your own `@` source](#write-your-own--source). They appear after the built-ins, in registration order, and disappear when the plugin that registered them is disposed. The plugin's group itself is registered with a **positive order**, so it is the last group in the menu: the bottom band belongs to this plugin, and the stock `dsh-client-ui-reference` source's file list stays above it.
58
58
 
59
59
  ## Interaction
60
60
 
@@ -71,6 +71,7 @@ The menu keeps five category rows in its **bottom band** — the rows nearest th
71
71
  - **Every draft reference the plugin can open is a link**: the test is the PLUGIN's own — the token decodes into a reference and the Host has confirmed the target exists — not whether the framework happened to decorate it. A hand-typed `@AGENTS.md` behaves exactly like one the editor recognised: the whole token is blue, a click anywhere inside it opens the reference, and the pointer turns into a hand. The colour is painted with the CSS Custom Highlight API (a Lexical text span expects exactly one text child, so wrapping any part of a token would break typing inside it) in the very colour the framework uses for its own references. A token whose target is **gone** is not left as ordinary text either: it is painted the dock's own **Missing** way — dimmed and struck through — because it is a reference that cannot open. **When** that verdict is drawn matters: only a **finished** token is judged (one followed by whitespace, or by another line, i.e. after Enter), because a token still being typed is every prefix of a path at once and `@N` is always "missing" — striking it through mid-word would judge a word the user has not written yet. A token that CAN open turns blue the moment its name is complete, exactly as the framework's own decoration does. A token whose verdict has not arrived, whose provider declared no `open`, or whose name cannot be placed claims nothing and stays ordinary text. On a browser without the highlight API nothing is painted at all — there only the part the framework itself coloured acts as a link, because nothing else is drawn as one.
72
72
  - **A reference another source inserted as a chip is clickable too**: the sidebar's file tree writes one into the draft as an atomic chip, which this client build draws in the framework's chip blue and leaves inert. Clicking one opens the file it names, and the chip then carries the plugin's link language. That source labels a chip with the file's **basename** (the full relative path lives only in the draft text), so a bare name is placed in two steps: first the plugin's own index, where a unique basename settles it; then, when the index itself is ambiguous (`index.ts` exists twice in this workspace), the **draft** — one token with that basename decides, two leave the chip inert. Only a name the index does not know at all is handed to the click's own existence check, which marks the chip stale rather than opening something wrong.
73
73
  - **References in sent messages are clickable**: a chip in a message bubble opens on click (files in the right Sidebar, the Host opener when there is none; skills through the skill source; provider items through the `open` their own provider declared). Only a chip that really opens is drawn as a link — the framework's own `--dsw-alias-link` blue with a hover underline — and a build that wires the chips itself renders `<button>`, which this plugin leaves alone. The plugin also implements the framework's `openReference` hook, so a client that activates a reference token in the composer asks its owning source to open it. The installed client activates nothing yet, so the plugin supplies that click itself, through the very action the chips run.
74
+ - **A session reference is a link**: identity here is decided by VALUE, because the id is not in the path. The wire form `@[label](dsh-session:…)` on its own names its session (the payload is decoded and must round-trip canonically), a label with spaces is still ONE token, and a hand-typed bare `@<session-id-or-title>` is promoted only on exactly ONE match from the session list — an ambiguous or unknown name stays inert, so `@AGENTS.md` stays the file reference it is. Clicking switches the current session through the same action a sidebar row performs. **Boundary**: a message already SENT can only be opened when its chip still carries the wire payload; once the Host folds it to a bare label the id is gone, which is why this plugin filed that upstream.
74
75
  - **Icons**: every row this plugin emits carries **its own** glyph — the same modern line set the reference dock uses, including per-file-type and per-language marks (TypeScript, Rust, PDF, image, archive, …) and a branch glyph for `@git`. The framework's menu row can only draw three glyphs of its own, so the plugin reserves the slot (which is what indents every name by the same amount) and draws the glyph itself, monochrome, in a fixed layer above the list: nothing is inserted into a row the framework owns, and the framework's own glyph is hidden only in the slots this layer actually covers.
75
76
 
76
77
  ![The @ menu](assets/screenshots/menu-mixed.png)
package/README.zh.md CHANGED
@@ -4,17 +4,17 @@
4
4
 
5
5
  本项目属于 **Side Quest(支线任务)** 工作室(**Side Quest Labs**)—— npm 范围 `@sidequest-007/*`,仓库 [`jameswatt139240-crypto/dsh-ATLAS`](https://github.com/jameswatt139240-crypto/dsh-ATLAS);**DSH 插件 id 沿用生态惯例的无 scope 形式 `dsh-*`**。
6
6
 
7
- <img src="assets/diagrams/atlas-overview.svg" alt="dsh-ATLAS:一个 @ 触发位、五个内置类别,以及任何插件都能注册的自己的类别" width="880">
7
+ <img src="assets/diagrams/atlas-overview.svg" alt="dsh-ATLAS:一个 @ 触发位、五个内置类别加内置的 git 类别,以及任何插件都能注册的自己的类别" width="880">
8
8
 
9
- **@ Last, All Sources.** 一个 `@` 。任何插件都能注册。
9
+ **@ Last, All Sources.** 一个 `@`,所有来源;一次点击,直达它指名的那个会话。
10
10
 
11
- DeepSeek Harness Web 界面的**统一 `@` 提及**插件:在输入框输入 `@`,即可引用工作区**文件与文件夹**、可发现的 **Skill**、**过去的聊天记录**、**已安装插件** —— 而且任何其它插件都能把自己的类别加进同一个菜单。
11
+ DeepSeek Harness Web 界面的**统一 `@` 提及**插件:在输入框输入 `@`,即可引用工作区**文件与文件夹**、可发现的 **Skill**、**过去的聊天记录**、**已安装插件**,以及**工作区的 git 改动** —— 而且任何其它插件都能把自己的类别加进同一个菜单。**会话**引用本身就是链接:点一下,输入框就切到那个会话。
12
12
 
13
- - **一个 `@`,所有来源**:五个内置类别 + 所有已注册的数据源,同一个列表;输入字母即跨类别一起搜。
13
+ - **一个 `@`,所有来源**:五个内置类别 + 内置的 `@git` provider(本部署里共 **6 个资源族**)+ 所有已注册的数据源,同一个列表;输入字母即跨类别一起搜。
14
14
  - **是平台,不是取词器**:第三方插件通过 `ctx.atlas` seam 注册自己的 `@` 类别。菜单每次打开都从**实时注册表**重建,所以**本包永远不需要为某个 provider 改代码** —— 见 [自己接一个 `@` 数据源](#自己接一个--数据源)。
15
15
  - **只给来源,不给内容**:提交的引用只注入标记(`<workspace-reference>`、`<skill-reference>`、`<atlas-reference>` …)。插件只读目录项元数据;文件内容由 agent 在需要时自己去读。
16
16
  - **治理写在代码里**:`scopes` 与 `testedOn` 必填、`id` 重复直接抛错、版本不匹配注册为 `verified: false`、provider 正文有预算(单条 16 KiB、单步 48 KiB)。
17
- - **`@git` 就是范例**:一个真实 provider,完全按第三方的方式实现,两个半边加起来约 200 行。
17
+ - **`@git` 既是范例,也是一个真实来源**:它像其它类别一样列出工作区的改动(路径 + 增删行数),并且完全按第三方的方式实现,两个半边加起来约 200 行。
18
18
 
19
19
  <p align="center"><img src="assets/diagrams/atlas-seam.svg" alt="@ 数据源 seam:浏览器半边在菜单打开时列候选,Host 半边在发送时解析一次,注册表负责门禁" width="880"></p>
20
20
 
@@ -40,7 +40,7 @@ dsh plugin --profile web add @sidequest-007/dsh-atlas
40
40
 
41
41
  安装或更新后**重启 `dsh web`**,确保 Host 与浏览器客户端都加载同一版本。然后在输入框打 `@`。
42
42
 
43
- ## 五个类别
43
+ ## 类别
44
44
 
45
45
  输入 `@` 后,菜单顶部是五个常驻类别行;也可以直接输入关键字跨类别混合搜索。
46
46
 
@@ -52,13 +52,13 @@ dsh plugin --profile web add @sidequest-007/dsh-atlas
52
52
  | 过去的聊天 | `chat:` | `C` | 官方 session-reference 解析器 | `@[标题](dsh-session:<id>)` |
53
53
  | 插件 | `plugin:` | `P` | 官方 plugin inventory | `@plugin:dsh-atlas` |
54
54
 
55
- **单个快捷键字母是"意图"而不是"查询"**:输入它只显示五个类别 + 一行「Tab 补全 → `类别:`」提示,且这行提示在**最下面**(高亮也已经落在它上面),按 <kbd>Tab</kbd> 或 <kbd>Enter</kbd> 即进入对应的 `类别:` 前缀。**注册的数据源也能有快捷键字母**:取 provider id 的首字母(`@git` → <kbd>G</kbd>),只有在该字母没被内置类别或更早注册的 provider 占用时才生效(`@<id>:` 前缀永远可用)。**过滤从更具体的东西开始**:不是快捷键的首字母,或第二个字符(`fi`/`fo`/`sk`/`ch`/`pl` 这类前缀本来就是两个字符)。点击类别行或「返回类别」行同样进入/返回,且**菜单保持打开**(这是本插件的两处覆写:正常 pick 路径会关闭菜单)。
55
+ **单个快捷键字母是"意图"而不是"查询"**:输入它只显示类别行——五个内置类别 + 内置的 `@git`——以及一行「Tab 补全 → `类别:`」提示,且这行提示在**最下面**(高亮也已经落在它上面),按 <kbd>Tab</kbd> 或 <kbd>Enter</kbd> 即进入对应的 `类别:` 前缀。**注册的数据源也能有快捷键字母**:取 provider id 的首字母(`@git` → <kbd>G</kbd>),只有在该字母没被内置类别或更早注册的 provider 占用时才生效(`@<id>:` 前缀永远可用)。**过滤从更具体的东西开始**:不是快捷键的首字母,或第二个字符(`fi`/`fo`/`sk`/`ch`/`pl` 这类前缀本来就是两个字符)。点击类别行或「返回类别」行同样进入/返回,且**菜单保持打开**(这是本插件的两处覆写:正常 pick 路径会关闭菜单)。
56
56
 
57
- **第六行起是"已注册数据源"。** `@git` 已内置(工作区的变更文件,引用内容就是该文件的 diff),任何插件都能用同样的方式加自己的类别——见[自己接一个 `@` 数据源](#自己接一个--数据源)。它们排在五个类别之后、按注册顺序出现,注册它们的插件被销毁时一起消失。插件自身的分组以**正的 `order`** 注册,因此它是菜单里的**最后一个分组**:底部那条工作带归本插件,框架自带 `dsh-client-ui-reference` 数据源的文件列表留在它上面。
57
+ **第六行起是"已注册数据源"。** `@git` 已内置(工作区的变更文件,引用内容就是该文件的 diff),任何插件都能用同样的方式加自己的类别——见[自己接一个 `@` 数据源](#自己接一个--数据源)。它们排在内置类别之后、按注册顺序出现,注册它们的插件被销毁时一起消失。插件自身的分组以**正的 `order`** 注册,因此它是菜单里的**最后一个分组**:底部那条工作带归本插件,框架自带 `dsh-client-ui-reference` 数据源的文件列表留在它上面。
58
58
 
59
59
  ## 交互
60
60
 
61
- - **直接输入字母**:跨类别混合搜索,结果按「最近引用」与「全部匹配」两段展示,每行带类别标签;五个类别行始终在**最下面的工作带**里可点选。
61
+ - **直接输入字母**:跨类别混合搜索,结果按「最近引用」与「全部匹配」两段展示,每行带类别标签;类别行(五个内置 + 内置的 `@git`)始终在**最下面的工作带**里可点选。
62
62
  - **文件夹**:`@folder:` 类别列出目录,选中后写入 `@路径/`(末尾的 `/` 才是「目录」的标记,插件依然只读目录项元数据);在斜杠后继续输入即可在该目录内收窄搜索。过滤时结果分**两档**排序:先按**名称**命中的文件夹(工作区内的在前,工作区外的在后),再是只有**路径**命中的那些,收进它们自己的 `路径匹配` 组。那一组默认展开、装得下就显示,折叠与否是你的手势。
63
63
  - **先写的文件夹会成为后面文件列表的作用域**:`@e:/work/docs/ @file:` 会先列出**该文件夹自己的文件**(一层,走的是文件夹 tab 同一套目录列表),组标题写明它来自哪个文件夹、并标明在工作区之外,然后才是工作区里的命中项。作用域是**位置**决定的——当前 token 之前最近的那个文件夹引用——所以**不需要连接词**;草稿里没有文件夹引用时行为与以前完全一致。
64
64
  - **分组折叠**:Skill 按管理层级(系统 / 用户 / 项目 / 自定义 / 插件)再按领域分组,插件按 npm scope(`@deepseek-ai/…` 或「其他」)分组,过去的聊天按工作区分组、子会话缩进在父会话下;组标题**点击即可折叠/展开;当高亮行正是该组标题时按 <kbd>Enter</kbd> 同样折叠/展开**。<kbd>Tab</kbd> 补全、<kbd>↑</kbd><kbd>↓</kbd>、<kbd>Esc</kbd> 全部由框架自己的 keymap 提供。除此之外插件只加三个键:高亮组标题上的 <kbd>Enter</kbd> 折叠、类别行/返回行选中后保持菜单打开、以及 <kbd>PageUp</kbd>/<kbd>PageDown</kbd> 按**实测一屏**翻页——翻页移动的是高亮(高亮属于别的数据源时就只滚列表),所以"看得见的那一行"永远是你正在操作的那一行。
@@ -72,6 +72,7 @@ dsh plugin --profile web add @sidequest-007/dsh-atlas
72
72
  - **草稿里任何能打开的 `@路径` 都是链接**:判断依据是**插件自己**——token 能解码成引用、并且宿主确认该目标存在——而不是框架有没有把它装饰成引用。所以手打的 `@AGENTS.md` 和框架认出来的 token 一样:**整条**变蓝、点任意位置都打开、鼠标变手型。上色用 CSS Custom Highlight API 逐段绘制(Lexical 的文本 span 只允许一个文本子节点,包裹尾巴会破坏输入),颜色取框架给引用用的同一个变量。**目标已不存在**的 token 也不会被当成普通文本,而是按引用栏那套「已失效」语言画成**暗色 + 删除线**——它是引用,只是打不开。**判定时机**:只有**写完的 token** 才会被判失效(后面跟着空白,或后面还有下一行=按过回车);正在输入的 token 不下结论——`@N` 这种中间态必然"不存在",边打边画删除线等于替用户还没写完的词下判决。**能打开**的 token 则在名字写成的那一刻就变蓝(与框架自己的引用装饰一样即时)。引用栏还没回答、provider 没声明 `open`、或名字放不下的 token 什么都不说、保持普通文本。浏览器不支持 highlight API 时什么都不绘制,此时**只有框架自己上过色的那部分**可点(没画成链接就不表现得像链接)。
73
73
  - **草稿里由其它数据源插入的 chip 也能点**:右侧栏的文件树就是往草稿里插一条原子 chip,这个客户端把它画成框架自己的 chip 蓝、并且不接任何点击。点它即打开它命名的文件,随后 chip 会带上插件的链接语言。那个数据源给 chip 的标签是**文件名**(完整相对路径只存在于草稿文本里),所以裸文件名按两步还原:①在插件索引里找同名 basename,唯一命中即用;②索引里就有歧义(例如 `index.ts` 在工作区里有两处)时回退到**草稿**——草稿里只有一个同名 token 就用它,仍有两个则**保持不可点**。索引完全不认识的名字才原样交给点击时的存在性检查(结果是标「已失效」,而不是打开一个错的目标)。
74
74
  - **已发送消息里的引用可点**:消息气泡里的引用 chip 点击即打开(文件进右侧栏,无右栏时交宿主打开器;Skill 交给 Skill 源;provider item 交给它自己声明的 `open`)。只有真的能打开的 chip 会被画成链接(用框架自己的 `--dsw-alias-link` 蓝 + 悬停下划线),框架自己接好点击的版本会渲染 `<button>`,本插件自动让位。插件同时实现了框架的 `openReference` 钩子:客户端在草稿里激活一个引用 token 时,由拥有它的 source 打开。安装版客户端还没有接上这一步,因此这个点击同样由插件补上,走的是 chip 用的那同一个动作。
75
+ - **会话引用本身就是链接**:它的身份按**值**判定,因为 id 不在路径里。`@[标签](dsh-session:…)` 这种带载荷的形态自己就能指明会话(载荷按 base64url 解码并要求**规范化回环**通过);**标签里有空格也算一个 token**;手打的裸 `@<会话id或标题>` 只有**唯一命中**会话列表时才提升为会话链接——歧义或查不到就保持普通文本,所以 `@AGENTS.md` 仍然是它本来那个文件引用。点击即切换当前会话,走的是**侧栏那一行的同一个动作**。**边界**:**已发送**的消息只有在 chip 上还留着载荷时才可点;一旦 Host 把它折成裸标签,id 就不在了——这条我已按上游通道提了报告。
75
76
  - **图标**:本插件发出的每一行都带**自己的**图标——与引用栏同一套现代线稿,含按文件类型与语言的标记(TypeScript、Rust、PDF、图片、压缩包…),`@git` 用分支图标。框架的菜单行只会画它自己的三种字形,所以插件**预定那个图标槽**(这决定了所有名字缩进一致),再由自己在列表上方的一层固定层里、用单色线条画出图标:不往框架拥有的行里插任何东西,框架自己的字形也只在被这一层真正覆盖的槽里隐藏。
76
77
 
77
78
  ![@ 菜单](assets/screenshots/menu-mixed.png)
@@ -1,4 +1,4 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="880" height="300" viewBox="0 0 880 300" role="img" aria-label="dsh-ATLAS: one @ trigger, five built-in categories with their glyphs, plus any plugin's own category">
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="880" height="300" viewBox="0 0 880 300" role="img" aria-label="dsh-ATLAS: one @ trigger, five built-in categories plus the bundled git category with their glyphs, and any plugin's own category; one click names the session it points at">
2
2
  <rect x="0" y="0" width="880" height="300" rx="18" fill="#0f1420"/>
3
3
  <rect x="0.5" y="0.5" width="879" height="299" rx="17.5" fill="none" stroke="#26304a"/>
4
4
 
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sidequest-007/dsh-atlas",
3
- "description": "@ Last, All Sources. One @ . Any plugin can register.",
4
- "version": "1.0.0",
3
+ "description": "@ Last, All Sources. One @, every source; one click, the session it names.",
4
+ "version": "1.0.2",
5
5
  "entry": {
6
6
  "name": "@sidequest-007/dsh-atlas",
7
7
  "inject": []
package/lib/client.js CHANGED
@@ -15709,10 +15709,54 @@ function menuGlyph(row, size = 16) {
15709
15709
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { size, stroke: 1.8, color: "currentColor", "aria-hidden": true });
15710
15710
  }
15711
15711
 
15712
+ // src/client/session-link.ts
15713
+ var SESSION_URI_SCHEME = "dsh-session:";
15714
+ var WIRE_MENTION_RE = /^@\[([^\]\n]*)\]\((dsh-session:[^)\s]+)\)$/u;
15715
+ function wireMentionAt(text, offset) {
15716
+ if (text[offset] !== "@") return void 0;
15717
+ const match = /@\[([^\]\n]*)\]\((dsh-session:[^)\s]+)\)(?=\s|$)/uy;
15718
+ match.lastIndex = offset;
15719
+ const found = match.exec(text);
15720
+ return found === null ? void 0 : { end: offset + found[0].length };
15721
+ }
15722
+ function wireSessionUri(text) {
15723
+ return WIRE_MENTION_RE.exec(text)?.[2] ?? "";
15724
+ }
15725
+ function decodeSessionUri(uri) {
15726
+ if (!uri.startsWith(SESSION_URI_SCHEME)) return void 0;
15727
+ const payload = uri.slice(SESSION_URI_SCHEME.length);
15728
+ if (!/^[A-Za-z0-9_-]+$/u.test(payload)) return void 0;
15729
+ let decoded;
15730
+ try {
15731
+ const base643 = payload.replace(/-/gu, "+").replace(/_/gu, "/");
15732
+ const binary = atob(base643);
15733
+ decoded = decodeURIComponent([...binary].map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`).join(""));
15734
+ } catch {
15735
+ return void 0;
15736
+ }
15737
+ let parsed;
15738
+ try {
15739
+ parsed = JSON.parse(decoded);
15740
+ } catch {
15741
+ return void 0;
15742
+ }
15743
+ if (typeof parsed !== "string" || parsed === "") return void 0;
15744
+ return encodeSessionUri(parsed) === uri ? parsed : void 0;
15745
+ }
15746
+ function encodeSessionUri(sessionId) {
15747
+ const json2 = JSON.stringify(sessionId);
15748
+ const bytes = new TextEncoder().encode(json2);
15749
+ let binary = "";
15750
+ for (const byte of bytes) binary += String.fromCharCode(byte);
15751
+ return SESSION_URI_SCHEME + btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
15752
+ }
15753
+
15712
15754
  // src/client/reference-links.ts
15713
15755
  var INERT_HANDLES = ["plugin:"];
15714
15756
  function decodeReferenceLink(title) {
15715
15757
  if (typeof title !== "string" || !title.startsWith("@")) return void 0;
15758
+ const session = decodeSessionUri(wireSessionUri(title));
15759
+ if (session !== void 0) return { kind: "session", sessionId: session };
15716
15760
  let label;
15717
15761
  if (title.startsWith('@"')) {
15718
15762
  if (title.length < 4 || !title.endsWith('"')) return void 0;
@@ -15745,6 +15789,9 @@ function decodeDraftReference(token) {
15745
15789
  const trimmed = token.endsWith("/") || token.endsWith("\\") ? token.slice(1, -1) : void 0;
15746
15790
  return trimmed === void 0 || trimmed === "" ? void 0 : { kind: "folder", path: trimmed };
15747
15791
  }
15792
+ function draftSessionId(token) {
15793
+ return decodeSessionUri(wireSessionUri(token));
15794
+ }
15748
15795
  var FILE_ADDRESS_PREFIX = "dsh-resource://file/";
15749
15796
  function encodeSegment(segment) {
15750
15797
  return encodeURIComponent(segment).replace(/%3A/giu, ":");
@@ -19303,6 +19350,13 @@ function localOffset(index, node, offset) {
19303
19350
  }
19304
19351
  function tokenRunAt(text, offset) {
19305
19352
  if (offset < 0 || offset > text.length) return void 0;
19353
+ const trigger = text.lastIndexOf("@", offset);
19354
+ if (trigger >= 0) {
19355
+ const wire = wireMentionAt(text, trigger);
19356
+ if (wire !== void 0 && offset <= wire.end) {
19357
+ return { token: text.slice(trigger, wire.end), start: trigger, end: wire.end };
19358
+ }
19359
+ }
19306
19360
  let start = offset;
19307
19361
  while (start > 0 && !isSpace(text[start - 1])) start -= 1;
19308
19362
  let end = offset;
@@ -19349,13 +19403,18 @@ function draftTokens() {
19349
19403
  }
19350
19404
  return out;
19351
19405
  }
19352
- function draftActivation(token, actionFor, verdict) {
19406
+ function draftActivation(token, actionFor, verdict, resolveLabel) {
19407
+ const session = draftSessionId(token) ?? (resolveLabel === void 0 || !token.startsWith("@") ? void 0 : resolveLabel(token.slice(1)));
19408
+ if (session !== void 0) return activate({ kind: "session", sessionId: session }, actionFor);
19353
19409
  const link = draftLink(token);
19354
19410
  if (link === void 0) return void 0;
19355
19411
  const resolved = withHostKind(link, verdict);
19356
19412
  if (resolved === void 0) return void 0;
19357
- const run = actionFor(resolved);
19358
- return run === void 0 ? void 0 : { link: resolved, run };
19413
+ return activate(resolved, actionFor);
19414
+ }
19415
+ function activate(link, actionFor) {
19416
+ const run = actionFor(link);
19417
+ return run === void 0 ? void 0 : { link, run };
19359
19418
  }
19360
19419
  function withHostKind(link, verdict) {
19361
19420
  if (link.kind !== "file" && link.kind !== "folder") return link;
@@ -19422,16 +19481,16 @@ function caretOf(event) {
19422
19481
  if (range == null) return void 0;
19423
19482
  return { node: range.startContainer, offset: range.startOffset };
19424
19483
  }
19425
- function DraftLinks({ actionFor, useReferenceInfo }) {
19484
+ function DraftLinks({ actionFor, useReferenceInfo, resolveSession }) {
19426
19485
  const infos = useReferenceInfo((snapshot) => snapshot.value);
19427
19486
  const verdictByPath = (0, import_react6.useMemo)(
19428
19487
  () => new Map(infos.map((info) => [referenceKey(info.relative), { exists: info.exists, kind: info.kind }])),
19429
19488
  [infos]
19430
19489
  );
19431
- const face = (0, import_react6.useRef)({ actionFor, verdict: (path) => verdictByPath.get(referenceKey(path)) });
19490
+ const face = (0, import_react6.useRef)({ actionFor, verdict: (path) => verdictByPath.get(referenceKey(path)), resolveSession });
19432
19491
  (0, import_react6.useEffect)(() => {
19433
- face.current = { actionFor, verdict: (path) => verdictByPath.get(referenceKey(path)) };
19434
- }, [actionFor, verdictByPath]);
19492
+ face.current = { actionFor, verdict: (path) => verdictByPath.get(referenceKey(path)), resolveSession };
19493
+ }, [actionFor, verdictByPath, resolveSession]);
19435
19494
  const repaint = (0, import_react6.useRef)(() => {
19436
19495
  });
19437
19496
  (0, import_react6.useEffect)(() => {
@@ -19440,12 +19499,12 @@ function DraftLinks({ actionFor, useReferenceInfo }) {
19440
19499
  const end = reference.decoratedEnd;
19441
19500
  if (end === void 0 || reference.local >= end) return void 0;
19442
19501
  }
19443
- return draftActivation(reference.token, face.current.actionFor, face.current.verdict);
19502
+ return draftActivation(reference.token, face.current.actionFor, face.current.verdict, face.current.resolveSession);
19444
19503
  };
19445
19504
  const paint = () => {
19446
19505
  const verdict = (path) => face.current.verdict(path);
19447
19506
  paintDraftLinks((token) => {
19448
- if (draftActivation(token.token, face.current.actionFor, verdict) !== void 0) return "link";
19507
+ if (draftActivation(token.token, face.current.actionFor, verdict, face.current.resolveSession) !== void 0) return "link";
19449
19508
  if (!token.settled) return void 0;
19450
19509
  return draftMissing(token.token, verdict) ? "missing" : void 0;
19451
19510
  });
@@ -19535,10 +19594,14 @@ var COMPOSER_CHIP_ATTRIBUTE = "data-composer-chip";
19535
19594
  var LINK_ATTRIBUTE = "data-dsh-atlas-link";
19536
19595
  var MISSING_ATTRIBUTE = "data-dsh-atlas-missing";
19537
19596
  var LINKABLE_KIND = "file";
19597
+ var SESSION_KIND = "session";
19538
19598
  function chipLink(chip) {
19539
19599
  if (chip.tagName === "BUTTON") return void 0;
19540
- if (chip.getAttribute(CHIP_ATTRIBUTE) !== LINKABLE_KIND) return void 0;
19541
- return decodeReferenceLink(chip.getAttribute("title"));
19600
+ const kind = chip.getAttribute(CHIP_ATTRIBUTE);
19601
+ if (kind !== LINKABLE_KIND && kind !== SESSION_KIND) return void 0;
19602
+ const link = decodeReferenceLink(chip.getAttribute("title"));
19603
+ if (link !== void 0 && kind === SESSION_KIND && link.kind !== "session") return void 0;
19604
+ return link;
19542
19605
  }
19543
19606
  function composerChipBody(host) {
19544
19607
  return host.querySelector("[title]") ?? host;
@@ -20101,6 +20164,22 @@ function apply(ctx) {
20101
20164
  const sessions = ctx.get("sessions");
20102
20165
  const t = ctx.locale.bind(NS);
20103
20166
  const entryByRel = /* @__PURE__ */ new Map();
20167
+ const resolveSessionLabel = (label) => {
20168
+ const snapshot = sessions.list.getSnapshot();
20169
+ const ids = snapshot.ids;
20170
+ if (ids.includes(label)) return label;
20171
+ const hits = ids.filter((id) => snapshot.byId[id]?.displayTitle === label);
20172
+ return hits.length === 1 ? String(hits[0]) : void 0;
20173
+ };
20174
+ const openSession = (sessionId) => {
20175
+ try {
20176
+ sessions.open(sessionId);
20177
+ return "opened";
20178
+ } catch (error51) {
20179
+ console.warn(`[dsh-atlas] this session reference could not be opened: ${sessionId}`, error51);
20180
+ return "gone";
20181
+ }
20182
+ };
20104
20183
  const scopeVerdicts = /* @__PURE__ */ new Map();
20105
20184
  const SCOPE_VERDICT_CACHE = 50;
20106
20185
  const search = async (sessionId, signal) => {
@@ -20336,6 +20415,9 @@ function apply(ctx) {
20336
20415
  return "opened";
20337
20416
  };
20338
20417
  }
20418
+ if (link.kind === "session") {
20419
+ return () => openSession(link.sessionId);
20420
+ }
20339
20421
  if (link.kind === "atlas") {
20340
20422
  const open = atlasRegistry.get(link.provider)?.provider.open;
20341
20423
  if (open === void 0) return void 0;
@@ -20442,7 +20524,8 @@ function apply(ctx) {
20442
20524
  order: 5,
20443
20525
  inject: (sessionId) => ({
20444
20526
  actionFor: (link) => actionFor(sessionId, link),
20445
- hooks: { referenceInfo }
20527
+ hooks: { referenceInfo },
20528
+ resolveSession: resolveSessionLabel
20446
20529
  })
20447
20530
  }, DraftLinks));
20448
20531
  ctx.slots.inject("conversation.input.overlay", () => ctx.slots.register({
@@ -1,6 +1,7 @@
1
1
  import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
2
  import type { ReferenceInfoSource } from './FilesDock.tsx';
3
3
  import type { ReferenceLink } from './reference-links.ts';
4
+ import type { SessionLabelResolver } from './session-link.ts';
4
5
  import type { ReferenceOutcome } from './ReferenceLinks.tsx';
5
6
  /** What the session's overlay shares with the draft bridge. */
6
7
  export interface DraftLinksInjected {
@@ -10,6 +11,13 @@ export interface DraftLinksInjected {
10
11
  readonly hooks: {
11
12
  readonly referenceInfo: ReferenceInfoSource;
12
13
  };
14
+ /**
15
+ * Resolve a BARE `@label` to the one session it names, or undefined. A session
16
+ * mention carries no id once it is durable, so this is the only way a draft
17
+ * token can become a session link — and it answers undefined for an ambiguous
18
+ * or unknown label, so a name that also exists as a file is never hijacked.
19
+ */
20
+ readonly resolveSession?: SessionLabelResolver | undefined;
13
21
  }
14
22
  /** Overlay entry props: the composer runtime face plus the opener and the verdicts. */
15
23
  export type DraftLinksProps = PropsRuntime<'conversation.input.overlay'> & InjectFace<DraftLinksInjected>;
@@ -18,4 +26,4 @@ export type DraftLinksProps = PropsRuntime<'conversation.input.overlay'> & Injec
18
26
  * @param props - the injected opener and the dock's verdict source.
19
27
  * @returns nothing; this entry renders no DOM of its own.
20
28
  */
21
- export declare function DraftLinks({ actionFor, useReferenceInfo }: DraftLinksProps): null;
29
+ export declare function DraftLinks({ actionFor, useReferenceInfo, resolveSession }: DraftLinksProps): null;
@@ -39,7 +39,12 @@ export type ReferenceLinksProps = PropsRuntime<'conversation.input.overlay'> & I
39
39
  *
40
40
  * A `<button>` chip means the framework passed its reference actions and wires
41
41
  * the click itself; acting as well would open the resource twice. A chip of any
42
- * other kind (session, folder, skill-slash, command) is not ours to open.
42
+ * other kind (folder, skill-slash, command) is not ours to open — EXCEPT a
43
+ * session chip, which is nobody else's: DSH renders every session mention as a
44
+ * plain label element, so switching sessions is exactly the click this bridge
45
+ * has to add. Its title has to carry the wire URI for that (the host folds the
46
+ * mention to a bare label before the message is durable), which
47
+ * `decodeReferenceLink` decides by value.
43
48
  * @param chip - the chip element.
44
49
  * @returns the decoded action, or undefined when the bridge must not act.
45
50
  */
@@ -1,4 +1,5 @@
1
1
  import { type ReferenceLink } from './reference-links.ts';
2
+ import { type SessionLabelResolver } from './session-link.ts';
2
3
  import type { ReferenceOutcome } from './ReferenceLinks.tsx';
3
4
  /** The attribute Lexical puts on the composer's contenteditable root. */
4
5
  export declare const EDITOR_SELECTOR = "[data-lexical-editor=\"true\"]";
@@ -59,6 +60,14 @@ export interface TokenRun {
59
60
  * The run is bounded by whitespace and by a second `@` (the grammar is `@` plus
60
61
  * a run of non-whitespace, non-`@` characters), so a click anywhere inside a
61
62
  * token — including its undecorated tail — resolves to the same whole token.
63
+ *
64
+ * ONE exception, and it is load-bearing: a complete WIRE session mention
65
+ * (`@[label with spaces](dsh-session:…)`) is returned whole, because the
66
+ * whitespace rule would otherwise cut it at its first space and leave a fragment
67
+ * (`@[继续`) that decodes as a file path — opening a nonexistent file (observed
68
+ * live as `cannot resolve target "…\继续": ENOENT`). The official mention
69
+ * vocabulary always spells a session reference this way, so the bracket form is
70
+ * the only spelling that can carry a label with spaces.
62
71
  * @param text - one line's text.
63
72
  * @param offset - the character offset the caret sits at.
64
73
  * @returns the token and its span, or undefined when no `@` governs that offset.
@@ -129,9 +138,12 @@ export declare function draftTokens(): readonly DraftToken[];
129
138
  * @param token - one draft token, `@` included.
130
139
  * @param actionFor - the session's action lookup.
131
140
  * @param verdict - the dock's last inspection verdict for one referenced path.
141
+ * @param resolveLabel - the session list lookup a BARE `@label` needs before it
142
+ * may become a session link; omitted means bare labels stay prose and fall
143
+ * through to the file rules.
132
144
  * @returns the action, or undefined when there is nothing to open.
133
145
  */
134
- export declare function draftActivation(token: string, actionFor: (link: ReferenceLink) => DraftAction | undefined, verdict: DraftVerdictLookup): DraftActivation | undefined;
146
+ export declare function draftActivation(token: string, actionFor: (link: ReferenceLink) => DraftAction | undefined, verdict: DraftVerdictLookup, resolveLabel?: SessionLabelResolver): DraftActivation | undefined;
135
147
  /** The Host's inspection verdict for one referenced path. */
136
148
  export interface DraftVerdict {
137
149
  /** Whether the target exists right now. */
@@ -1,12 +1,3 @@
1
- /**
2
- * The two pure pieces of the sent-message click-through: decoding one rendered
3
- * reference chip back into the action it can offer, and spelling the file
4
- * address the right Sidebar resolves.
5
- *
6
- * Both are pure string work so the browser bridge (`ReferenceLinks.tsx`) stays a
7
- * thin event adapter, and so the address grammar can be pinned against the
8
- * Harness's own implementation in the tests instead of being trusted by eye.
9
- */
10
1
  /** One mention of an already sent message that a click can act on. */
11
2
  export type ReferenceLink = {
12
3
  readonly kind: 'file';
@@ -25,6 +16,14 @@ export type ReferenceLink = {
25
16
  readonly kind: 'atlas';
26
17
  readonly provider: string;
27
18
  readonly item: string;
19
+ }
20
+ /**
21
+ * A SESSION reference: selecting it switches the current session, which is a
22
+ * main-area navigation rather than a resource the Sidebar could show.
23
+ */
24
+ | {
25
+ readonly kind: 'session';
26
+ readonly sessionId: string;
28
27
  };
29
28
  /**
30
29
  * Decode one reference chip's raw label (its `title`) into the action it offers.
@@ -34,10 +33,11 @@ export type ReferenceLink = {
34
33
  * `@atlas:provider/item`, `@[label](dsh-session:…)`, or an out-of-workspace
35
34
  * absolute path (`@E:\…` / `@E:/…`, whose drive letter is a path, not a handle).
36
35
  * Only a workspace file, a quoted path, an absolute path, a skill (whose source
37
- * the skill source opens) and a provider item (whose meaning belongs to that
38
- * provider) name something to act on. A trailing separator is a folder mention
39
- * (its chip kind is `folder`), and a colon in the first segment is our own handle
40
- * spelling unless it is a drive letter.
36
+ * the skill source opens), a provider item (whose meaning belongs to that
37
+ * provider) and a wire session mention (whose payload names its session) name
38
+ * something to act on. A trailing separator is a folder mention (its chip kind
39
+ * is `folder`), and a colon in the first segment is our own handle spelling
40
+ * unless it is a drive letter.
41
41
  * @param title - the chip's `title` attribute (the undecorated label).
42
42
  * @returns the mention, or undefined when there is nothing to open.
43
43
  */
@@ -49,10 +49,25 @@ export declare function decodeReferenceLink(title: string | null | undefined): R
49
49
  * decorates by syntax rather than by name: a trailing separator marks a folder
50
50
  * mention (`@src/`), whose chip kind is its own and whose click opens the
51
51
  * directory through the Host opener.
52
+ *
53
+ * A bare `@label` is deliberately left to the caller's
54
+ * `resolveSessions`: opening one as a session needs the live session list, and
55
+ * a token that names no session must still fall through to being a file path
56
+ * (`@docs` is a directory long before it is a session title). The wire session
57
+ * form needs no list, so it is decoded right here.
52
58
  * @param token - the activated token, trigger included (e.g. `@src/a.ts`).
53
59
  * @returns the mention, or undefined when nothing can be opened.
54
60
  */
55
61
  export declare function decodeDraftReference(token: string): ReferenceLink | undefined;
62
+ /**
63
+ * The session one DRAFT token names, when the token itself says so.
64
+ *
65
+ * Only the wire form answers here: a bare label needs {@link SessionLabelResolver}
66
+ * and the live list, which is the caller's business (see `draftActivation`).
67
+ * @param token - one draft token, `@` included.
68
+ * @returns the session id to switch to, or undefined when the token carries none.
69
+ */
70
+ export declare function draftSessionId(token: string): string | undefined;
56
71
  /**
57
72
  * The address of a workspace-relative file in one session: the Harness's
58
73
  * `dsh-resource://file/session/<sessionId>/<path>` form, spelled here so the
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Session-reference CLICK-THROUGH: decode the two shapes a session mention
3
+ * leaves behind, and decide the one session (if any) each names.
4
+ *
5
+ * Why this module exists at all: a session mention is folded to a bare label
6
+ * before the message is durable (`parseSessionReferenceText` replaces
7
+ * `@[label](dsh-session:…)` with `@label`), and the editor keeps the wire form
8
+ * as plain TEXT rather than an atomic chip — measured live, so neither the
9
+ * bubble's chip `title` nor the composer's DOM carries a session id. Two shapes
10
+ * therefore survive, and each needs its own rule:
11
+ *
12
+ * - the WIRE form `@[label](dsh-session:<base64url(JSON string)>)`, which names
13
+ * its session exactly, and
14
+ * - a bare `@label`, which names nothing by itself and is opened ONLY when the
15
+ * session list contains exactly ONE matching session (id or title). A guess
16
+ * would hijack a real file: `@AGENTS.md` is a file long before it is a
17
+ * session, and the plugin must never turn a workspace path into a jump.
18
+ *
19
+ * Pure string + list work, so the browser bridge stays a thin event adapter and
20
+ * the rules are pinned by their own spec.
21
+ */
22
+ /** One session reference a click can act on. */
23
+ export interface SessionLink {
24
+ readonly sessionId: string;
25
+ /** The session a bare label resolves to, for the "no id in the label" shape. */
26
+ readonly resolvedFrom?: string;
27
+ }
28
+ /** The wire mention, captured as label + payload. Matches the framework's own grammar. */
29
+ export declare const WIRE_MENTION_RE: RegExp;
30
+ /**
31
+ * The wire mention that STARTS at one character offset of a line, if any.
32
+ *
33
+ * The composer's token reader is whitespace-bounded, which is exactly why this
34
+ * exists: a session label routinely contains spaces (`@[继续 LoongCrush 项目的任务](…)`)
35
+ * and the whitespace rule would cut the mention at its first space, leaving a
36
+ * fragment that decodes as a FILE path. `$` is replaced by a lookahead so the
37
+ * regex can be anchored at a position inside a longer line.
38
+ * @param text - one line's text.
39
+ * @param offset - the offset the mention would start at (the `@`).
40
+ * @returns the mention and its end offset, or undefined when none starts there.
41
+ */
42
+ export declare function wireMentionAt(text: string, offset: number): {
43
+ readonly end: number;
44
+ } | undefined;
45
+ /**
46
+ * The `dsh-session:` URI inside one wire mention, or undefined for any other
47
+ * spelling. Shared so the sent-message decoder and the draft activation can ask
48
+ * the same question instead of each keeping its own copy of the grammar.
49
+ * @param text - a chip `title` or a draft token.
50
+ * @returns the URI, ready for {@link decodeSessionUri}.
51
+ */
52
+ export declare function wireSessionUri(text: string): string;
53
+ /**
54
+ * Decode one `dsh-session:` URI into the session id it names.
55
+ *
56
+ * The payload is base64url of `JSON.stringify(id)` (the host's canonical
57
+ * encoding), so a payload that decodes to anything but one JSON string is not a
58
+ * session address — and the round trip is verified, exactly as the host's own
59
+ * decoder does, so a non-canonical spelling can never name a different session.
60
+ * @param uri - a complete `dsh-session:` URI.
61
+ * @returns the session id, or undefined when the URI is malformed.
62
+ */
63
+ export declare function decodeSessionUri(uri: string): string | undefined;
64
+ /** Look one label up against the live session list. */
65
+ export interface SessionLinkDeps {
66
+ /**
67
+ * The session one mention label names, or undefined when it names none or
68
+ * more than one. Implemented over `ctx.sessions.list`, so the plugin never
69
+ * guesses: ambiguity answers undefined and the reference stays inert.
70
+ */
71
+ matchLabel(label: string): string | undefined;
72
+ }
73
+ /**
74
+ * The lookup a DRAFT asks before it may promote a bare `@label` into a session
75
+ * link, or undefined for a bridge that may not promote one at all. Injection
76
+ * passes the plugin's `ctx.sessions.list` reader; everything else (a test, the
77
+ * sent-message bridge) leaves it out and bare labels stay dead letters.
78
+ */
79
+ export type SessionLabelResolver = (label: string) => string | undefined;
80
+ /**
81
+ * Resolve one reference label (a chip `title`, or a draft token without its
82
+ * `@`… see the callers) into a session to open.
83
+ *
84
+ * The wire form is exact and needs no list. A bare label is accepted ONLY in
85
+ * the draft, where the user's own typing is the intent: in a SENT bubble the
86
+ * same bare label also decorates plain prose (`@AGENTS.md` is routinely a
87
+ * file), so promoting it there would overrule a file reference the user made.
88
+ * @param text - the label with its leading `@`, or the full wire mention.
89
+ * @param deps - the live session lookup.
90
+ * @param allowBareLabel - whether a bare `@label` may be promoted (draft only).
91
+ * @returns the session to open, or undefined when nothing may be opened.
92
+ */
93
+ export declare function resolveSessionLink(text: string, deps: SessionLinkDeps, allowBareLabel: boolean): SessionLink | undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sidequest-007/dsh-atlas",
3
- "description": "@ Last, All Sources. One @ . Any plugin can register.",
4
- "version": "1.0.1",
3
+ "description": "@ Last, All Sources. One @, every source; one click, the session it names.",
4
+ "version": "1.0.2",
5
5
  "license": "MIT",
6
6
  "author": "Side Quest Labs",
7
7
  "publishConfig": {