thincoder 0.12.61 → 0.12.63
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 +34 -1
- package/README.md +13 -12
- package/bin/thincoder.mjs +63 -28
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -371
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +32 -10
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +88 -0
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +10 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +31 -4
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +206 -0
- package/src/tui/index.mjs +40 -12
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +85 -0
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +17 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +49 -17
- package/src/tui/subagent-blocks.mjs +21 -3
- package/src/tui/subagent-children.mjs +86 -14
- package/src/tui/subagent-freeze.mjs +80 -3
- package/src/tui/suspension-drive.mjs +48 -22
- package/src/tui/tool-args.mjs +5 -2
- package/src/tui/tool-display.mjs +16 -2
- package/src/tui/tool-events.mjs +64 -22
- package/src/tui/tui-lifecycle.mjs +9 -2
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +21 -5
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -242
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -228
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -191
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -468
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -289
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -294
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -479
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -453
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -413
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -381
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -83
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -413
- package/src/memory/core.mjs +0 -300
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -417
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -217
- package/src/prompts/discipline-normal.md +0 -179
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -55
- package/src/prompts/persona-engineering.md +0 -54
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -214
- package/src/session-guard.mjs +0 -47
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-slots.mjs +0 -489
- package/src/session.mjs +0 -475
- package/src/skills.mjs +0 -153
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -224
package/src/tools/lsp.mjs
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tools/lsp.mjs — LSP (Language Server Protocol) code intelligence tool
|
|
3
|
-
* Zero-dependency JSON-RPC 2.0 over stdio client.
|
|
4
|
-
*
|
|
5
|
-
* Provides: go-to-definition, find-references, hover info, document symbols, diagnostics.
|
|
6
|
-
* Lazy-starts language servers on first call. Configurable via config.json lsp.servers.
|
|
7
|
-
*
|
|
8
|
-
* Config format:
|
|
9
|
-
* "lsp": {
|
|
10
|
-
* "servers": {
|
|
11
|
-
* "typescript": { "command": "typescript-language-server", "args": ["--stdio"] },
|
|
12
|
-
* "python": { "command": "pyright-langserver", "args": ["--stdio"] }
|
|
13
|
-
* }
|
|
14
|
-
* }
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { spawn } from "node:child_process"
|
|
18
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
19
|
-
import { join, extname } from "node:path"
|
|
20
|
-
import { DESC } from "./shared.mjs"
|
|
21
|
-
|
|
22
|
-
// ---- JSON-RPC transport over stdio ----
|
|
23
|
-
|
|
24
|
-
/** Send a JSON-RPC request to the server via stdin */
|
|
25
|
-
function send(proc, message) {
|
|
26
|
-
const body = JSON.stringify(message)
|
|
27
|
-
const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`
|
|
28
|
-
proc.stdin.write(header + body)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Read one JSON-RPC message from stdout. Returns parsed JSON, or null on EOF. */
|
|
32
|
-
function readMessage(proc) {
|
|
33
|
-
return new Promise((resolve) => {
|
|
34
|
-
let header = ""
|
|
35
|
-
let contentLength = -1
|
|
36
|
-
|
|
37
|
-
const onData = (chunk) => {
|
|
38
|
-
if (contentLength < 0) {
|
|
39
|
-
header += chunk.toString()
|
|
40
|
-
const match = header.match(/Content-Length: (\d+)\r\n\r\n/)
|
|
41
|
-
if (match) {
|
|
42
|
-
contentLength = parseInt(match[1])
|
|
43
|
-
const bodyStart = header.indexOf("\r\n\r\n") + 4
|
|
44
|
-
const remaining = header.slice(bodyStart)
|
|
45
|
-
header = ""
|
|
46
|
-
if (remaining.length >= contentLength) {
|
|
47
|
-
proc.stdout.removeListener("data", onData)
|
|
48
|
-
try { resolve(JSON.parse(remaining.slice(0, contentLength))) } catch { resolve(null) }
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
// Need more data — leave remaining in a buffer-like state
|
|
52
|
-
proc.stdout.removeListener("data", onData)
|
|
53
|
-
readBody(proc, remaining, contentLength).then(resolve)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
proc.stdout.on("data", onData)
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function readBody(proc, buf, targetLen) {
|
|
63
|
-
return new Promise((resolve) => {
|
|
64
|
-
const onData = (chunk) => {
|
|
65
|
-
buf += chunk.toString()
|
|
66
|
-
if (buf.length >= targetLen) {
|
|
67
|
-
proc.stdout.removeListener("data", onData)
|
|
68
|
-
try { resolve(JSON.parse(buf.slice(0, targetLen))) } catch { resolve(null) }
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
proc.stdout.on("data", onData)
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Send a request and wait for the matching response */
|
|
76
|
-
async function request(proc, method, params, id) {
|
|
77
|
-
send(proc, { jsonrpc: "2.0", id, method, params })
|
|
78
|
-
while (true) {
|
|
79
|
-
const msg = await readMessage(proc)
|
|
80
|
-
if (!msg) return null
|
|
81
|
-
if (msg.id === id) return msg
|
|
82
|
-
// Store notifications for later retrieval (diagnostics)
|
|
83
|
-
if (msg.method === "textDocument/publishDiagnostics") {
|
|
84
|
-
proc._diagnostics = proc._diagnostics || {}
|
|
85
|
-
proc._diagnostics[msg.params.uri] = msg.params.diagnostics
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Send a notification (no response expected) */
|
|
91
|
-
function notify(proc, method, params) {
|
|
92
|
-
send(proc, { jsonrpc: "2.0", method, params })
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---- Language server process management ----
|
|
96
|
-
|
|
97
|
-
const servers = new Map() // ext → { proc, rootUri, ready }
|
|
98
|
-
|
|
99
|
-
/** Convert file path to file:// URI */
|
|
100
|
-
function toUri(absPath) {
|
|
101
|
-
return "file:///" + absPath.replace(/\\/g, "/").replace(/^([A-Z]):/, (_, d) => d.toLowerCase() + "%3A")
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Resolve which language server to use for a file extension */
|
|
105
|
-
function resolveServerConfig(config, ext) {
|
|
106
|
-
const map = {
|
|
107
|
-
".js": "typescript", ".mjs": "typescript", ".cjs": "typescript",
|
|
108
|
-
".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
109
|
-
".py": "python", ".pyi": "python",
|
|
110
|
-
".rs": "rust",
|
|
111
|
-
".go": "go",
|
|
112
|
-
}
|
|
113
|
-
const key = map[ext] || ext.slice(1)
|
|
114
|
-
const servers = config?.lsp?.servers ?? {}
|
|
115
|
-
return servers[key] || null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Start or reuse a language server for the given file */
|
|
119
|
-
async function getServer(cwd, filePath, config) {
|
|
120
|
-
const ext = extname(filePath).toLowerCase()
|
|
121
|
-
const srvConfig = resolveServerConfig(config, ext)
|
|
122
|
-
if (!srvConfig) return null
|
|
123
|
-
|
|
124
|
-
const abs = join(cwd, ...filePath.split("/"))
|
|
125
|
-
if (!existsSync(abs)) return null
|
|
126
|
-
|
|
127
|
-
const rootUri = toUri(cwd)
|
|
128
|
-
const key = `${ext}:${cwd}`
|
|
129
|
-
let entry = servers.get(key)
|
|
130
|
-
|
|
131
|
-
if (entry && entry.ready) return entry
|
|
132
|
-
|
|
133
|
-
// Start new server
|
|
134
|
-
const proc = spawn(srvConfig.command, srvConfig.args || [], {
|
|
135
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
136
|
-
windowsHide: true,
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
proc.stderr.on("data", () => {}) // suppress stderr noise
|
|
140
|
-
|
|
141
|
-
entry = { proc, rootUri, ready: false }
|
|
142
|
-
servers.set(key, entry)
|
|
143
|
-
|
|
144
|
-
// Initialize handshake
|
|
145
|
-
const initResult = await request(proc, "initialize", {
|
|
146
|
-
processId: null,
|
|
147
|
-
rootUri,
|
|
148
|
-
capabilities: {
|
|
149
|
-
textDocument: {
|
|
150
|
-
definition: { linkSupport: false },
|
|
151
|
-
references: {},
|
|
152
|
-
hover: { contentFormat: ["plaintext"] },
|
|
153
|
-
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
workspace: {},
|
|
157
|
-
}, 1)
|
|
158
|
-
|
|
159
|
-
if (!initResult) {
|
|
160
|
-
proc.kill()
|
|
161
|
-
servers.delete(key)
|
|
162
|
-
return null
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
notify(proc, "initialized", {})
|
|
166
|
-
entry.ready = true
|
|
167
|
-
return entry
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Notify the server that a file is open (required before queries) */
|
|
171
|
-
async function ensureOpen(proc, uri, ext) {
|
|
172
|
-
const langMap = { ".js": "javascript", ".mjs": "javascript", ".cjs": "javascript", ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript", ".py": "python", ".rs": "rust", ".go": "go" }
|
|
173
|
-
const languageId = langMap[ext] || ext.slice(1)
|
|
174
|
-
notify(proc, "textDocument/didOpen", {
|
|
175
|
-
textDocument: {
|
|
176
|
-
uri,
|
|
177
|
-
languageId,
|
|
178
|
-
version: 1,
|
|
179
|
-
text: readFileSync(uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":").replace(/\//g, "\\"), "utf8"),
|
|
180
|
-
},
|
|
181
|
-
})
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ---- Tool definition ----
|
|
185
|
-
|
|
186
|
-
export const lspTool = {
|
|
187
|
-
name: "lsp",
|
|
188
|
-
description: DESC("lsp"),
|
|
189
|
-
parameters: {
|
|
190
|
-
type: "object",
|
|
191
|
-
properties: {
|
|
192
|
-
subcommand: {
|
|
193
|
-
type: "string",
|
|
194
|
-
enum: ["definition", "references", "hover", "symbols", "diagnostics"],
|
|
195
|
-
description: "LSP operation to perform",
|
|
196
|
-
},
|
|
197
|
-
uri: {
|
|
198
|
-
type: "string",
|
|
199
|
-
description: "Target file path (relative to project root)",
|
|
200
|
-
},
|
|
201
|
-
line: {
|
|
202
|
-
type: "integer",
|
|
203
|
-
description: "1-based line number (for definition/references/hover)",
|
|
204
|
-
},
|
|
205
|
-
character: {
|
|
206
|
-
type: "integer",
|
|
207
|
-
description: "1-based character offset (for definition/references/hover)",
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
required: ["subcommand", "uri"],
|
|
211
|
-
},
|
|
212
|
-
readonly: true,
|
|
213
|
-
|
|
214
|
-
async execute(args, ctx) {
|
|
215
|
-
const config = ctx.agent?.config ?? {}
|
|
216
|
-
const cwd = ctx.cwd
|
|
217
|
-
const filePath = args.uri
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
const entry = await getServer(cwd, filePath, config)
|
|
221
|
-
if (!entry) {
|
|
222
|
-
const ext = extname(filePath).toLowerCase()
|
|
223
|
-
return `No LSP server configured for "${ext}" files. Add one to config.json:\n"lsp": { "servers": { "${ext.slice(1)}": { "command": "...", "args": ["--stdio"] } } }`
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const { proc } = entry
|
|
227
|
-
const abs = join(cwd, ...filePath.split("/"))
|
|
228
|
-
const uri = toUri(abs)
|
|
229
|
-
const ext = extname(filePath).toLowerCase()
|
|
230
|
-
await ensureOpen(proc, uri, ext)
|
|
231
|
-
|
|
232
|
-
// Wait briefly for diagnostics to arrive
|
|
233
|
-
await new Promise((r) => setTimeout(r, 300))
|
|
234
|
-
|
|
235
|
-
switch (args.subcommand) {
|
|
236
|
-
case "definition": {
|
|
237
|
-
if (!args.line || !args.character) return "Error: line and character required for definition"
|
|
238
|
-
const res = await request(proc, "textDocument/definition", {
|
|
239
|
-
textDocument: { uri },
|
|
240
|
-
position: { line: args.line - 1, character: args.character - 1 },
|
|
241
|
-
}, 10)
|
|
242
|
-
if (!res?.result) return "(no definition found)"
|
|
243
|
-
const locs = Array.isArray(res.result) ? res.result : [res.result]
|
|
244
|
-
return locs.map((l) => {
|
|
245
|
-
const path = l.uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":")
|
|
246
|
-
return `${path}:${l.range.start.line + 1}:${l.range.start.character + 1}`
|
|
247
|
-
}).join("\n")
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
case "references": {
|
|
251
|
-
if (!args.line || !args.character) return "Error: line and character required for references"
|
|
252
|
-
const res = await request(proc, "textDocument/references", {
|
|
253
|
-
textDocument: { uri },
|
|
254
|
-
position: { line: args.line - 1, character: args.character - 1 },
|
|
255
|
-
context: { includeDeclaration: false },
|
|
256
|
-
}, 10)
|
|
257
|
-
if (!res?.result?.length) return "(no references found)"
|
|
258
|
-
return res.result.slice(0, 50).map((l) => {
|
|
259
|
-
const path = l.uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":")
|
|
260
|
-
return `${path}:${l.range.start.line + 1}:${l.range.start.character + 1}`
|
|
261
|
-
}).join("\n") + (res.result.length > 50 ? `\n... and ${res.result.length - 50} more` : "")
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
case "hover": {
|
|
265
|
-
if (!args.line || !args.character) return "Error: line and character required for hover"
|
|
266
|
-
const res = await request(proc, "textDocument/hover", {
|
|
267
|
-
textDocument: { uri },
|
|
268
|
-
position: { line: args.line - 1, character: args.character - 1 },
|
|
269
|
-
}, 10)
|
|
270
|
-
if (!res?.result?.contents) return "(no hover info)"
|
|
271
|
-
const contents = res.result.contents
|
|
272
|
-
if (typeof contents === "string") return contents
|
|
273
|
-
if (Array.isArray(contents)) return contents.map((c) => typeof c === "string" ? c : c.value).join("\n")
|
|
274
|
-
if (contents.value) return contents.value
|
|
275
|
-
return JSON.stringify(contents)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
case "symbols": {
|
|
279
|
-
const res = await request(proc, "textDocument/documentSymbol", {
|
|
280
|
-
textDocument: { uri },
|
|
281
|
-
}, 10)
|
|
282
|
-
if (!res?.result?.length) return "(no symbols found)"
|
|
283
|
-
function render(nodes, depth) {
|
|
284
|
-
const lines = []
|
|
285
|
-
for (const n of nodes) {
|
|
286
|
-
const kind = n.kind != null ? ` [${symbolKind(n.kind)}]` : ""
|
|
287
|
-
lines.push(`${" ".repeat(depth)}${n.name}${kind} — L${n.range.start.line + 1}`)
|
|
288
|
-
if (n.children?.length) lines.push(...render(n.children, depth + 1))
|
|
289
|
-
}
|
|
290
|
-
return lines
|
|
291
|
-
}
|
|
292
|
-
return render(res.result, 0).join("\n")
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
case "diagnostics": {
|
|
296
|
-
const diags = proc._diagnostics?.[uri]
|
|
297
|
-
if (!diags?.length) return "(no diagnostics)"
|
|
298
|
-
return diags.slice(0, 30).map((d) => {
|
|
299
|
-
const sev = { 1: "ERROR", 2: "WARN", 3: "INFO", 4: "HINT" }[d.severity] || "?"
|
|
300
|
-
return `L${d.range.start.line + 1}: ${sev}: ${d.message}${d.code ? ` [${d.code}]` : ""}`
|
|
301
|
-
}).join("\n") + (diags.length > 30 ? `\n... and ${diags.length - 30} more` : "")
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
default:
|
|
305
|
-
return `Unknown subcommand: ${args.subcommand}`
|
|
306
|
-
}
|
|
307
|
-
} catch (err) {
|
|
308
|
-
return `lsp error: ${err.message}`
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function symbolKind(k) {
|
|
314
|
-
const kinds = { 1: "file", 2: "module", 3: "namespace", 4: "package", 5: "class", 6: "method", 7: "property", 8: "field", 9: "constructor", 10: "enum", 11: "interface", 12: "function", 13: "variable", 14: "constant", 15: "string", 16: "number", 17: "boolean", 18: "array", 19: "object", 20: "key", 21: "null", 22: "enumMember", 23: "struct", 24: "event", 25: "operator", 26: "typeParameter" }
|
|
315
|
-
return kinds[k] || `kind-${k}`
|
|
316
|
-
}
|
package/src/tools/ops.mjs
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ops.mjs — operational tools: file_ops (move/copy/rename), process (list),
|
|
3
|
-
* get_current_time, wait_for (condition wait). Each exists so the model reaches
|
|
4
|
-
* for a dedicated tool instead of shelling out to `bash` for the same operation
|
|
5
|
-
* (parity with thinworker).
|
|
6
|
-
*/
|
|
7
|
-
import { DESC, resolveInCwd, truncate } from "./shared.mjs"
|
|
8
|
-
import { cp, rename, rm } from "node:fs/promises"
|
|
9
|
-
import { existsSync } from "node:fs"
|
|
10
|
-
import { execFileSync } from "node:child_process"
|
|
11
|
-
import net from "node:net"
|
|
12
|
-
|
|
13
|
-
// ─── file_ops ──────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
export const fileOpsTool = {
|
|
16
|
-
name: "file_ops",
|
|
17
|
-
description: DESC("file_ops"),
|
|
18
|
-
parameters: {
|
|
19
|
-
type: "object",
|
|
20
|
-
properties: {
|
|
21
|
-
action: { type: "string", enum: ["move", "copy", "rename"], description: "move | copy | rename" },
|
|
22
|
-
source: { type: "string", description: "Source path, relative to cwd or absolute" },
|
|
23
|
-
dest: { type: "string", description: "Destination path" },
|
|
24
|
-
},
|
|
25
|
-
required: ["action", "source", "dest"],
|
|
26
|
-
},
|
|
27
|
-
readonly: false,
|
|
28
|
-
async execute({ action, source, dest }, ctx) {
|
|
29
|
-
if (typeof source !== "string" || !source) return "Error: source is required"
|
|
30
|
-
if (typeof dest !== "string" || !dest) return "Error: dest is required"
|
|
31
|
-
if (!["move", "copy", "rename"].includes(action)) return `Error: action must be move | copy | rename (got "${action}")`
|
|
32
|
-
const src = resolveInCwd(ctx, source)
|
|
33
|
-
const dst = resolveInCwd(ctx, dest)
|
|
34
|
-
if (src === dst) return "Error: source and dest resolve to the same path"
|
|
35
|
-
|
|
36
|
-
if (action === "copy") {
|
|
37
|
-
await cp(src, dst, { recursive: true, force: true })
|
|
38
|
-
return `Copied ${source} → ${dest}`
|
|
39
|
-
}
|
|
40
|
-
// move & rename share the rename syscall; cross-device move falls back to copy+rm.
|
|
41
|
-
try {
|
|
42
|
-
await rename(src, dst)
|
|
43
|
-
} catch (e) {
|
|
44
|
-
if (e?.code !== "EXDEV") throw e
|
|
45
|
-
await cp(src, dst, { recursive: true, force: true })
|
|
46
|
-
await rm(src, { recursive: true, force: true })
|
|
47
|
-
}
|
|
48
|
-
return `${action === "rename" ? "Renamed" : "Moved"} ${source} → ${dest}`
|
|
49
|
-
},
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ─── process ───────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
export const processTool = {
|
|
55
|
-
name: "process",
|
|
56
|
-
description: DESC("process"),
|
|
57
|
-
parameters: {
|
|
58
|
-
type: "object",
|
|
59
|
-
properties: {
|
|
60
|
-
name: { type: "string", description: "Optional name substring filter (case-insensitive)" },
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
readonly: true,
|
|
64
|
-
async execute({ name }, ctx) {
|
|
65
|
-
const filter = typeof name === "string" && name.trim() ? name.trim().toLowerCase() : null
|
|
66
|
-
let rows
|
|
67
|
-
try {
|
|
68
|
-
rows = process.platform === "win32" ? listWindows() : listPosix()
|
|
69
|
-
} catch (e) {
|
|
70
|
-
return `process listing failed: ${e?.message ?? String(e)}`
|
|
71
|
-
}
|
|
72
|
-
if (filter) rows = rows.filter((r) => r.name.toLowerCase().includes(filter))
|
|
73
|
-
if (rows.length === 0) return filter ? `No running processes match "${name}"` : "(no processes)"
|
|
74
|
-
return truncate(rows.map((r) => `${r.name}\tPID ${r.pid}${r.mem ? `\t${r.mem}` : ""}`).join("\n"))
|
|
75
|
-
},
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function listWindows() {
|
|
79
|
-
// tasklist /FO CSV /NH → lines: "name.exe","1234","Console","1","12,345 K"
|
|
80
|
-
const out = execFileSync("tasklist", ["/FO", "CSV", "/NH"], { encoding: "utf8", timeout: 10000 })
|
|
81
|
-
const rows = []
|
|
82
|
-
for (const line of out.split("\n")) {
|
|
83
|
-
const parts = line.split('","')
|
|
84
|
-
if (parts.length < 2) continue
|
|
85
|
-
const name = parts[0].replace(/^"/, "").trim()
|
|
86
|
-
const pid = parts[1].replace(/"/, "").trim()
|
|
87
|
-
const mem = parts[4] ? parts[4].replace(/"/, "").trim() : ""
|
|
88
|
-
if (!name || !pid) continue
|
|
89
|
-
rows.push({ name, pid, mem })
|
|
90
|
-
}
|
|
91
|
-
return rows
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function listPosix() {
|
|
95
|
-
const out = execFileSync("ps", ["-eo", "pid=,comm="], { encoding: "utf8", timeout: 10000 })
|
|
96
|
-
const rows = []
|
|
97
|
-
for (const line of out.split("\n")) {
|
|
98
|
-
const m = line.trim().match(/^(\d+)\s+(.+)$/)
|
|
99
|
-
if (m) rows.push({ name: m[2], pid: m[1], mem: "" })
|
|
100
|
-
}
|
|
101
|
-
return rows
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ─── get_current_time ──────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
export const getCurrentTimeTool = {
|
|
107
|
-
name: "get_current_time",
|
|
108
|
-
description: DESC("get_current_time"),
|
|
109
|
-
parameters: { type: "object", properties: {} },
|
|
110
|
-
readonly: true,
|
|
111
|
-
async execute() {
|
|
112
|
-
const now = new Date()
|
|
113
|
-
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "unknown"
|
|
114
|
-
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
|
115
|
-
return `Date: ${now.toISOString()} (UTC)\nTimezone: ${tz}\nWeekday: ${days[now.getDay()]}\nLocal: ${now.toLocaleString()}`
|
|
116
|
-
},
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ─── wait_for ───────────────────────────────────────────────────
|
|
120
|
-
|
|
121
|
-
/** wait_for bounds (TOOLS.md §16): a BUILT-IN ceiling replaces the unbounded
|
|
122
|
-
* sleep-then-wait — timeout_ms defaults to 30s (config.json agent.waitForTimeoutMs
|
|
123
|
-
* overrides), capped at WAIT_FOR_MAX_TIMEOUT_MS (600s — the execute-tool timeout
|
|
124
|
-
* convention, Math.min(t, 600_000)); interval_ms defaults to 1s with a 100ms
|
|
125
|
-
* floor so polling never busy-spins. */
|
|
126
|
-
export const WAIT_FOR_DEFAULT_TIMEOUT_MS = 30_000
|
|
127
|
-
export const WAIT_FOR_MAX_TIMEOUT_MS = 600_000
|
|
128
|
-
export const WAIT_FOR_DEFAULT_INTERVAL_MS = 1_000
|
|
129
|
-
export const WAIT_FOR_MIN_INTERVAL_MS = 100
|
|
130
|
-
|
|
131
|
-
// Condition-source seam (§16 评审 #4): production uses the real evaluator
|
|
132
|
-
// (evaluateWaitForCondition); tests inject a deterministic fake source via
|
|
133
|
-
// setWaitForConditionSource (default null — production path unchanged).
|
|
134
|
-
let injectedConditionSource = null
|
|
135
|
-
export function setWaitForConditionSource(source) {
|
|
136
|
-
injectedConditionSource = typeof source === "function" ? source : null
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Parse a wait_for condition into { kind, arg }. Unknown conditions are an
|
|
140
|
-
* EXPLICIT error enumerating the supported forms (评审 #2 — never a silent
|
|
141
|
-
* wait on a misspelled condition). */
|
|
142
|
-
export function parseWaitForCondition(condition) {
|
|
143
|
-
const c = String(condition ?? "").trim()
|
|
144
|
-
let m
|
|
145
|
-
if (c === "advisor settled") return { kind: "advisor", arg: null }
|
|
146
|
-
if (c === "consult done") return { kind: "consult", arg: null }
|
|
147
|
-
if ((m = c.match(/^subagent id:\s*(\d+) done$/))) return { kind: "subagent", arg: String(Number(m[1])) }
|
|
148
|
-
if ((m = c.match(/^file exists:\s*(.+)$/))) return { kind: "file", arg: m[1].trim() }
|
|
149
|
-
if ((m = c.match(/^port open:\s*(\d+)$/))) return { kind: "port", arg: Number(m[1]) }
|
|
150
|
-
throw new Error(
|
|
151
|
-
`wait_for: unsupported condition "${c}" — supported: "advisor settled", "subagent id:N done", "consult done", "file exists:path", "port open:N"`,
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Async-subagent pool of an agent — ctx.agent 可达性(评审 #1):dispatch 把
|
|
156
|
-
* agent 实例放进工具 ctx(agent/dispatch.mjs toolCtx.agent),池挂在 agent 上
|
|
157
|
-
* (深度 0 的 VS Code 侧 agent._asyncSubagents 与 history._asyncSubagents 同
|
|
158
|
-
* 一 Map——见 agent.mjs;CLI 侧直接是 agent._asyncSubagents)。 */
|
|
159
|
-
function asyncPool(agent) {
|
|
160
|
-
if (!agent) return new Map()
|
|
161
|
-
if (agent._asyncSubagents instanceof Map) return agent._asyncSubagents
|
|
162
|
-
return agent.history?._asyncSubagents instanceof Map ? agent.history._asyncSubagents : new Map()
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** "subagent id:N done" — the async entry for N settled (status done / done flag)
|
|
166
|
-
* or already left the pool (moved to pending results / digested / never spawned —
|
|
167
|
-
* nothing is left to wait on). Absent ids are done by vacuity: the model waits on
|
|
168
|
-
* ids its own spawn ack just returned, so an id no longer in the pool means done.
|
|
169
|
-
* Dual key lookup (2026-09-06 advisor #1): VS Code pool keys are the numeric
|
|
170
|
-
* spawn-time id, the CLI keys them as String(id) — accept both. */
|
|
171
|
-
function asyncEntryDone(agent, id) {
|
|
172
|
-
const pool = asyncPool(agent)
|
|
173
|
-
const key = String(id)
|
|
174
|
-
const entry = pool.get(key) ?? pool.get(Number(key)) ?? null
|
|
175
|
-
if (!entry) return true
|
|
176
|
-
return entry.done === true || entry.status === "done"
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** 评审池双载体(第 10 批 §18.3 #2):agent ∪ history——未初始化的载体不进集合。 */
|
|
180
|
-
function advisorPools(agent) {
|
|
181
|
-
return [agent?._asyncAdvisors, agent?.history?._asyncAdvisors].filter((m) => m instanceof Map)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** Any entry matching pred that is still running/queued (not done) — in the given pool. */
|
|
185
|
-
function hasRunningIn(pool, pred) {
|
|
186
|
-
for (const e of pool?.values() ?? []) {
|
|
187
|
-
if (pred(e) && !(e.done === true || e.status === "done")) return true
|
|
188
|
-
}
|
|
189
|
-
return false
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** "advisor settled" 真判据(第 10 批 §18.3 #2 修正):**评审池真实态**——双载体
|
|
193
|
-
* (agent._asyncAdvisors ∪ history._asyncAdvisors)无 running/queued 条目(与 §11.2 的
|
|
194
|
-
* 未决评审判定 advisorReviewPending 同源语义)。修前读**子代理池**的 role==="advisor"
|
|
195
|
-
* 条目——评审条目只在 _asyncAdvisors → 恒无命中 → 恒真 0ms 秒过(用户实证)。 */
|
|
196
|
-
function advisorSettled(agent) {
|
|
197
|
-
return !advisorPools(agent).some((pool) => hasRunningIn(pool, (e) => e.status === "running" || e.status === "queued"))
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** "consult done" — every consult session has drained (pending 0) or was
|
|
201
|
-
* explicitly stopped (its children were aborted). No sessions → true (vacuously). */
|
|
202
|
-
function consultAllDone(agent) {
|
|
203
|
-
const sessions = agent?._consultSessions
|
|
204
|
-
if (!sessions || sessions.size === 0) return true
|
|
205
|
-
for (const s of sessions.values()) {
|
|
206
|
-
if (!s.stopped && (s.pending ?? 0) > 0) return false
|
|
207
|
-
}
|
|
208
|
-
return true
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** "port open:N" — 127.0.0.1 probe with a short connect timeout (never hangs a poll). */
|
|
212
|
-
function portOpenProbe(port) {
|
|
213
|
-
return new Promise((resolve) => {
|
|
214
|
-
const socket = net.connect({ host: "127.0.0.1", port, timeout: 400 })
|
|
215
|
-
let settled = false
|
|
216
|
-
const finish = (v) => {
|
|
217
|
-
if (settled) return
|
|
218
|
-
settled = true
|
|
219
|
-
socket.destroy()
|
|
220
|
-
resolve(v)
|
|
221
|
-
}
|
|
222
|
-
socket.once("connect", () => finish(true))
|
|
223
|
-
socket.once("error", () => finish(false))
|
|
224
|
-
socket.once("timeout", () => finish(false))
|
|
225
|
-
})
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Real condition evaluator (production path) — returns a boolean; throws on an
|
|
229
|
-
* unsupported condition string. Polling is async (await setTimeout) — the event
|
|
230
|
-
* loop keeps running so background async work (subagents/consult) can settle
|
|
231
|
-
* between polls (评审 #1 — no blocking spin). */
|
|
232
|
-
export async function evaluateWaitForCondition(condition, ctx) {
|
|
233
|
-
const parsed = parseWaitForCondition(condition)
|
|
234
|
-
const agent = ctx?.agent
|
|
235
|
-
switch (parsed.kind) {
|
|
236
|
-
case "advisor":
|
|
237
|
-
// 第 10 批修正(§18.3 #2——修前恒真 0ms 秒过):判据 = 评审池真实态(双载体)——
|
|
238
|
-
// 不再读子代理池的 role==="advisor" 条目(该池永无此类条目);语义与 §11.2 未决
|
|
239
|
-
// 评审判定同源(advisorReviewPending)。
|
|
240
|
-
return advisorSettled(agent)
|
|
241
|
-
case "subagent":
|
|
242
|
-
return asyncEntryDone(agent, parsed.arg)
|
|
243
|
-
case "consult":
|
|
244
|
-
return consultAllDone(agent)
|
|
245
|
-
case "file":
|
|
246
|
-
return existsSync(resolveInCwd(ctx, parsed.arg))
|
|
247
|
-
case "port":
|
|
248
|
-
return await portOpenProbe(parsed.arg)
|
|
249
|
-
default:
|
|
250
|
-
return false
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function evalConditionSource(condition, ctx) {
|
|
255
|
-
if (injectedConditionSource) return injectedConditionSource(condition, ctx)
|
|
256
|
-
return evaluateWaitForCondition(condition, ctx)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export const waitForTool = {
|
|
260
|
-
name: "wait_for",
|
|
261
|
-
description: DESC("wait_for"),
|
|
262
|
-
parameters: {
|
|
263
|
-
type: "object",
|
|
264
|
-
properties: {
|
|
265
|
-
condition: { type: "string", description: 'Condition expression — "advisor settled", "subagent id:N done", "consult done", "file exists:path", "port open:N"' },
|
|
266
|
-
interval_ms: { type: "integer", description: "Poll interval in ms (default 1000, floor 100)" },
|
|
267
|
-
timeout_ms: { type: "integer", description: "Overall timeout in ms (default 30000, cap 600000; config.json agent.waitForTimeoutMs overrides the default)" },
|
|
268
|
-
},
|
|
269
|
-
required: ["condition"],
|
|
270
|
-
},
|
|
271
|
-
readonly: true,
|
|
272
|
-
async execute(args, ctx = {}) {
|
|
273
|
-
const condition = typeof args?.condition === "string" ? args.condition.trim() : ""
|
|
274
|
-
if (!condition) return "Error: condition is required"
|
|
275
|
-
const intervalMs = Math.max(WAIT_FOR_MIN_INTERVAL_MS, Math.floor(args?.interval_ms ?? WAIT_FOR_DEFAULT_INTERVAL_MS))
|
|
276
|
-
const cfgTimeout = ctx?.agent?.config?.agent?.waitForTimeoutMs
|
|
277
|
-
const base = Number.isFinite(args?.timeout_ms)
|
|
278
|
-
? args.timeout_ms
|
|
279
|
-
: Number.isFinite(cfgTimeout) && cfgTimeout > 0 ? cfgTimeout : WAIT_FOR_DEFAULT_TIMEOUT_MS
|
|
280
|
-
const timeoutMs = Math.min(Math.max(1, Math.floor(base)), WAIT_FOR_MAX_TIMEOUT_MS)
|
|
281
|
-
const started = Date.now()
|
|
282
|
-
let polls = 0
|
|
283
|
-
for (;;) {
|
|
284
|
-
// Cancel/interrupt-safe exit(T-W7):Ctrl+C / 定向 abort 传播——不吞信号、
|
|
285
|
-
// 不把用户停变成工具错误(dispatch 对 aborted signal 原样再抛)。
|
|
286
|
-
if (ctx.signal?.aborted) throw new Error("wait_for: interrupted by abort signal")
|
|
287
|
-
polls++
|
|
288
|
-
const ok = await evalConditionSource(condition, ctx)
|
|
289
|
-
if (ok === true) {
|
|
290
|
-
return `wait_for: condition satisfied after ${Date.now() - started}ms (${polls} check${polls === 1 ? "" : "s"}): "${condition}"`
|
|
291
|
-
}
|
|
292
|
-
const remaining = started + timeoutMs - Date.now()
|
|
293
|
-
if (remaining <= 0) {
|
|
294
|
-
return `wait_for: timed out after ${timeoutMs}ms waiting for "${condition}" (${polls} checks — condition never became true)`
|
|
295
|
-
}
|
|
296
|
-
await new Promise((r) => setTimeout(r, Math.min(intervalMs, remaining)))
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
}
|