@lyra_wang/dsh-terminal-context 0.0.0-stage → 0.4.0
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/README.md +47 -2
- package/client.js +85 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +1 -0
- package/index.js +96 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +38 -3
package/README.md
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lyra_wang/dsh-terminal-context
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把 **DSH 侧边栏终端**当前屏幕的内容交给 Agent,让它下一步的指令依据你真实的终端状态,
|
|
4
|
+
而不是凭空安排。
|
|
5
|
+
|
|
6
|
+
## 它解决什么
|
|
7
|
+
|
|
8
|
+
你在侧边栏终端里跑命令时,Agent 看不到你敲了什么、看到了什么报错、在哪个目录。
|
|
9
|
+
于是它给的指令经常"对着空气":让你 `cd` 到已经在的目录、重复你已经做过的步骤、
|
|
10
|
+
或者在你已经失败的前提下继续往下走。
|
|
11
|
+
|
|
12
|
+
装上之后,Agent 能读到当前屏幕,于是:
|
|
13
|
+
|
|
14
|
+
- 知道你**在哪个目录**(不用你说)
|
|
15
|
+
- 知道你**上一条命令的结果**(成功/报错)
|
|
16
|
+
- 你跑完之后,它能**接着结果往下做**
|
|
17
|
+
|
|
18
|
+
## 怎么工作
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
界面侧(同一页面) Host 侧
|
|
22
|
+
读 .xterm-rows 渲染出的可见行 → POST /plugins/terminal-context/push → 内存缓存
|
|
23
|
+
↓
|
|
24
|
+
Agent 工具 read_terminal_context
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**只读**:不改终端、不发输入、不落盘(终端里可能有 token,进程结束就没了)。
|
|
28
|
+
|
|
29
|
+
## 已知限制
|
|
30
|
+
|
|
31
|
+
- **只能读到当前可见的一屏**(最多 60 行)。向上滚出屏幕的缓冲读不到 —— 这是"看屏幕"这条路的固有代价。
|
|
32
|
+
- 依赖 xterm 的 DOM 类名(`.xterm-rows`)。xterm 大改版会失效,失效表现是"没数据",不会影响 DSH 启动。
|
|
33
|
+
- 内容**变了才推**,每 2 秒检查一次。
|
|
34
|
+
|
|
35
|
+
## 安全设计
|
|
36
|
+
|
|
37
|
+
`inject: []` —— 界面插件不声明任何服务依赖。这是刻意的:DSH 的界面插件如果声明了一个
|
|
38
|
+
不存在的服务,入口会永久 pending,**导致整个界面起不来**(v0.1.0 踩过这个坑)。
|
|
39
|
+
不声明依赖,最坏情况是"插件没反应",不会拖垮应用。
|
|
40
|
+
|
|
41
|
+
## 要求
|
|
42
|
+
|
|
43
|
+
- DSH 桌面端
|
|
44
|
+
- 侧边栏打开过终端
|
|
45
|
+
|
|
46
|
+
## 许可
|
|
47
|
+
|
|
48
|
+
MIT
|
package/client.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 界面侧 · 读侧边栏(v0.4.0)
|
|
3
|
+
*
|
|
4
|
+
* 原来只读终端的 .xterm-rows。但侧边栏不止终端 —— 还可能挂着浏览器面板,
|
|
5
|
+
* 两者都是"你眼前的东西",都该进 Agent 的上下文。
|
|
6
|
+
*
|
|
7
|
+
* 做法:先按已知结构读终端;再扫一遍可能的侧边栏容器,把它们的结构一起推回去。
|
|
8
|
+
* 为什么要推结构:不同面板的 DOM 差别很大,与其猜选择器,不如让页面自己报告
|
|
9
|
+
* "有哪些容器、各有多少字",下一版照着写就一次到位。
|
|
10
|
+
*
|
|
11
|
+
* 三条硬约束(v0.1 事故的教训):
|
|
12
|
+
* inject: [] 不声明依赖 → 不可能 pending → 不会拖垮启动
|
|
13
|
+
* 全部 try/catch 读失败只是"没数据",不白屏
|
|
14
|
+
* 只读 不点击、不输入
|
|
15
|
+
*/
|
|
16
|
+
window.__ModuleLoader__.load({
|
|
17
|
+
id: '@lyra_wang/dsh-terminal-context',
|
|
18
|
+
factory() {
|
|
19
|
+
const MAX = 80
|
|
20
|
+
const clean = (t) => (t || '').replace(/\u00a0/g, ' ').replace(/[ \t]+$/gm, '')
|
|
21
|
+
|
|
22
|
+
const readTerminal = () => {
|
|
23
|
+
const rows = document.querySelectorAll('.xterm-rows > div')
|
|
24
|
+
if (!rows.length) return null
|
|
25
|
+
const out = []
|
|
26
|
+
for (const r of rows) out.push(clean(r.textContent))
|
|
27
|
+
while (out.length && !out[0].trim()) out.shift()
|
|
28
|
+
while (out.length && !out[out.length - 1].trim()) out.pop()
|
|
29
|
+
return out.slice(-MAX)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 扫可能的侧边栏容器。命中后连结构和样本一起带回去,供下一版锁定选择器。
|
|
33
|
+
const scanPanels = () => {
|
|
34
|
+
const hits = []
|
|
35
|
+
const seen = new Set()
|
|
36
|
+
const nodes = document.querySelectorAll('[class*="sidebar"],[class*="rightbar"],[class*="panel"],[class*="terminal"],[class*="browser"],[id*="sidebar"],[id*="panel"]')
|
|
37
|
+
for (const el of nodes) {
|
|
38
|
+
if (seen.has(el) || el === document.body) continue
|
|
39
|
+
seen.add(el)
|
|
40
|
+
const text = clean(el.innerText || '')
|
|
41
|
+
if (text.length < 40) continue
|
|
42
|
+
hits.push({
|
|
43
|
+
tag: el.tagName.toLowerCase(),
|
|
44
|
+
cls: String(el.className || '').slice(0, 80),
|
|
45
|
+
chars: text.length,
|
|
46
|
+
sample: text.slice(0, 120).replace(/\n+/g, ' ⏎ '),
|
|
47
|
+
})
|
|
48
|
+
if (hits.length >= 8) break
|
|
49
|
+
}
|
|
50
|
+
return hits
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
inject: [],
|
|
55
|
+
apply(ctx) {
|
|
56
|
+
let lastKey = ''
|
|
57
|
+
const push = () => {
|
|
58
|
+
try {
|
|
59
|
+
const lines = readTerminal()
|
|
60
|
+
const panels = scanPanels()
|
|
61
|
+
const key = JSON.stringify([lines && lines.slice(-5), panels.map((p) => p.cls + p.chars)])
|
|
62
|
+
if (key === lastKey) return
|
|
63
|
+
lastKey = key
|
|
64
|
+
fetch('/plugins/terminal-context/push', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'content-type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
lines: lines || [],
|
|
69
|
+
truncated: !!(lines && lines.length >= MAX),
|
|
70
|
+
diag: panels.length ? { panels } : null,
|
|
71
|
+
}),
|
|
72
|
+
}).catch(() => {})
|
|
73
|
+
} catch { /* 静默 */ }
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
ctx.effect(() => {
|
|
77
|
+
const first = setTimeout(push, 1200)
|
|
78
|
+
const timer = setInterval(push, 2000)
|
|
79
|
+
return () => { clearTimeout(first); clearInterval(timer) }
|
|
80
|
+
})
|
|
81
|
+
} catch { /* ignore */ }
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
})
|
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#4D6BFE" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9l3 3-3 3"/><path d="M13 15h4"/></svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host half of terminal-context.
|
|
3
|
+
*
|
|
4
|
+
* The browser half subscribes to `terminalController.attach` (a Remote-only
|
|
5
|
+
* stream a Host plugin cannot open) and pushes the visible lines here. This
|
|
6
|
+
* half keeps only the most recent push in memory and exposes it to the agent
|
|
7
|
+
* through a tool. Nothing is written to disk: the terminal may contain
|
|
8
|
+
* secrets, so the buffer dies with the process.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const PUSH_PATH = '/plugins/terminal-context/push'
|
|
12
|
+
const MAX_LINES = 200
|
|
13
|
+
|
|
14
|
+
let latest = { updatedAt: 0, terminalId: '', cwd: '', lines: [], truncated: false, diag: null }
|
|
15
|
+
|
|
16
|
+
function send(res, status, payload) {
|
|
17
|
+
if (res && typeof res.writeHead === 'function') {
|
|
18
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
19
|
+
res.end(JSON.stringify(payload))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
return new Response(JSON.stringify(payload), {
|
|
23
|
+
status,
|
|
24
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readBody(req) {
|
|
29
|
+
if (req && typeof req[Symbol.asyncIterator] === 'function') {
|
|
30
|
+
return (async () => {
|
|
31
|
+
const chunks = []
|
|
32
|
+
for await (const chunk of req) chunks.push(chunk)
|
|
33
|
+
return Buffer.concat(chunks).toString('utf8')
|
|
34
|
+
})()
|
|
35
|
+
}
|
|
36
|
+
if (req && typeof req.text === 'function') return req.text()
|
|
37
|
+
return Promise.resolve('')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const inject = ['tools', 'webServer']
|
|
41
|
+
|
|
42
|
+
export function apply(ctx) {
|
|
43
|
+
ctx.effect(() => ctx.webServer.register({
|
|
44
|
+
kind: 'exact',
|
|
45
|
+
path: PUSH_PATH,
|
|
46
|
+
handler: async (req, res) => {
|
|
47
|
+
try {
|
|
48
|
+
const body = JSON.parse(await readBody(req))
|
|
49
|
+
const lines = Array.isArray(body.lines) ? body.lines.slice(-MAX_LINES).map(String) : []
|
|
50
|
+
latest = {
|
|
51
|
+
diag: body.diag || null,
|
|
52
|
+
updatedAt: Date.now(),
|
|
53
|
+
terminalId: String(body.terminalId || ''),
|
|
54
|
+
cwd: String(body.cwd || ''),
|
|
55
|
+
lines,
|
|
56
|
+
truncated: Boolean(body.truncated),
|
|
57
|
+
}
|
|
58
|
+
return send(res, 200, { ok: true, lines: lines.length })
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return send(res, 400, { ok: false, error: String((error && error.message) || error) })
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
}), 'terminal-context: push route')
|
|
64
|
+
|
|
65
|
+
ctx.effect(() => {
|
|
66
|
+
const dispose = ctx.tools.register({
|
|
67
|
+
name: 'read_terminal_context',
|
|
68
|
+
description:
|
|
69
|
+
'Return the last lines the user saw in their sidebar terminal, plus its working directory and how fresh it is. Use before proposing a command when the user is working in a terminal, and after they say a command finished.',
|
|
70
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
71
|
+
output: {
|
|
72
|
+
schema: { type: 'object', additionalProperties: true },
|
|
73
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
74
|
+
},
|
|
75
|
+
async execute() {
|
|
76
|
+
if (!latest.updatedAt) {
|
|
77
|
+
return {
|
|
78
|
+
available: false,
|
|
79
|
+
reason: 'browser half has not pushed anything yet',
|
|
80
|
+
hint: 'the terminal-context client module must be installed and the page reloaded',
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
available: true,
|
|
85
|
+
ageSeconds: Math.round((Date.now() - latest.updatedAt) / 1000),
|
|
86
|
+
terminalId: latest.terminalId,
|
|
87
|
+
cwd: latest.cwd,
|
|
88
|
+
truncated: latest.truncated,
|
|
89
|
+
diag: latest.diag,
|
|
90
|
+
lines: latest.lines,
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
return () => dispose()
|
|
95
|
+
})
|
|
96
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyra_wang/dsh-terminal-context",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "把 DSH 侧边栏当前显示的内容(终端 / 浏览器面板)交给 Agent,作为它判断当前处境的上下文。",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js",
|
|
8
|
+
"./locale/*.json": "./locale/*.json",
|
|
9
|
+
"./icon": "./icon.svg",
|
|
10
|
+
"./client": "./client.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"client.js",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"locale/*.json",
|
|
17
|
+
"icon.svg"
|
|
18
|
+
],
|
|
19
|
+
"dsh": {
|
|
20
|
+
"bundle": {
|
|
21
|
+
"patch": "./cordis.patch.yml"
|
|
22
|
+
},
|
|
23
|
+
"client": {
|
|
24
|
+
"platform": "web",
|
|
25
|
+
"immediately": true,
|
|
26
|
+
"inject": []
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/LyraWang6688/ai-capability-store.git",
|
|
33
|
+
"directory": "external-capabilities/plugin/dsh/terminal-context"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"dsh",
|
|
37
|
+
"dsh-plugin",
|
|
38
|
+
"terminal",
|
|
39
|
+
"context"
|
|
40
|
+
]
|
|
6
41
|
}
|