@lyra_wang/dsh-terminal-context 0.0.0-stage → 0.3.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 +65 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +1 -0
- package/index.js +96 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 界面侧 · 读终端画面(v0.3.0)
|
|
3
|
+
*
|
|
4
|
+
* 走的是"看屏幕"这条路,不是接口:终端是 xterm 渲染的 DOM,和对话在同一个页面里,
|
|
5
|
+
* 所以直接读渲染出来的行即可 —— 和"豆包看着 Word 的界面操作 Word"同一个思路。
|
|
6
|
+
*
|
|
7
|
+
* 为什么不用 terminalController:客户端 `ctx` 上只有 `fiber`,服务名找不到;
|
|
8
|
+
* 而 DOM 这条路不依赖任何内部 API。
|
|
9
|
+
*
|
|
10
|
+
* 三条硬约束(上次事故的教训):
|
|
11
|
+
* 1. inject: [] —— 没有依赖,就不可能 pending,不可能拖垮启动
|
|
12
|
+
* 2. 全部 try/catch —— 一个异常不该让界面白屏
|
|
13
|
+
* 3. 只读 —— 不改终端、不发输入
|
|
14
|
+
*/
|
|
15
|
+
window.__ModuleLoader__.load({
|
|
16
|
+
id: '@lyra_wang/dsh-terminal-context',
|
|
17
|
+
factory() {
|
|
18
|
+
const MAX_LINES = 60
|
|
19
|
+
const readScreen = () => {
|
|
20
|
+
// xterm 把可见区渲染成 .xterm-rows 下的若干行
|
|
21
|
+
const rows = document.querySelectorAll('.xterm-rows > div')
|
|
22
|
+
if (!rows.length) {
|
|
23
|
+
const screen = document.querySelector('.xterm-screen')
|
|
24
|
+
if (!screen) return null
|
|
25
|
+
const text = (screen.innerText || '').split('\n')
|
|
26
|
+
return text.slice(-MAX_LINES)
|
|
27
|
+
}
|
|
28
|
+
const lines = []
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
const text = (row.textContent || '').replace(/\u00a0/g, ' ').replace(/\s+$/, '')
|
|
31
|
+
lines.push(text)
|
|
32
|
+
}
|
|
33
|
+
while (lines.length && !lines[0].trim()) lines.shift()
|
|
34
|
+
while (lines.length && !lines[lines.length - 1].trim()) lines.pop()
|
|
35
|
+
return lines.slice(-MAX_LINES)
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
inject: [],
|
|
39
|
+
apply(ctx) {
|
|
40
|
+
let last = ''
|
|
41
|
+
const push = () => {
|
|
42
|
+
try {
|
|
43
|
+
const lines = readScreen()
|
|
44
|
+
if (lines === null) return
|
|
45
|
+
const joined = lines.join('\n')
|
|
46
|
+
if (joined === last) return
|
|
47
|
+
last = joined
|
|
48
|
+
fetch('/plugins/terminal-context/push', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'content-type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({ lines, truncated: lines.length >= MAX_LINES, diag: null }),
|
|
52
|
+
}).catch(() => {})
|
|
53
|
+
} catch { /* 静默:读屏失败不能影响界面 */ }
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
ctx.effect(() => {
|
|
57
|
+
const first = setTimeout(push, 1200)
|
|
58
|
+
const timer = setInterval(push, 2000)
|
|
59
|
+
return () => { clearTimeout(first); clearInterval(timer) }
|
|
60
|
+
})
|
|
61
|
+
} catch { /* ignore */ }
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
})
|
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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "meta": { "title": "DSH Terminal Context", "description": "Feeds the DSH sidebar terminal's recent output to the agent so its next step matches what is actually on screen." } }
|
package/locale/zh.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "meta": { "title": "DSH 终端上下文", "description": "把 DSH 侧边栏终端最近若干行交给 Agent,让它给的下一步依据你当前的真实处境。" } }
|
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.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "把侧边栏终端最近若干行交给 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
|
}
|