@miphamai/cli 0.59.0 → 0.60.1
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/package.json +1 -1
- package/src/commands/autoloop-journal.ts +42 -2
- package/src/config/defaults.ts +3 -0
- package/src/core/engine.ts +65 -2
- package/src/core/permission.ts +45 -0
- package/src/i18n-core/locales/en-US.json +4 -0
- package/src/i18n-core/locales/zh-CN.json +4 -0
- package/src/providers/openai-compat.ts +1 -1
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +11 -0
- package/src/tools/computer/app-launcher.ts +7 -1
- package/src/tools/computer/screenshot.ts +7 -1
- package/src/tools/scheduling/schedule-wakeup.ts +15 -0
- package/src/ui/app.tsx +349 -219
- package/src/ui/command-picker.tsx +20 -13
- package/src/ui/command-token.ts +10 -0
- package/src/ui/commands.ts +22 -7
- package/src/ui/input.tsx +11 -3
- package/src/ui/loop-noop.ts +23 -0
- package/src/ui/thinking.ts +21 -0
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ function ensureDir(): void {
|
|
|
8
8
|
if (!existsSync(AUTOLOOP_DIR)) mkdirSync(AUTOLOOP_DIR, { recursive: true })
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
interface AutoloopJournal {
|
|
11
|
+
export interface AutoloopJournal {
|
|
12
12
|
sessionId: string
|
|
13
13
|
prompt: string
|
|
14
14
|
status: 'active' | 'completed' | 'stopped'
|
|
@@ -16,6 +16,9 @@ interface AutoloopJournal {
|
|
|
16
16
|
startedAt: string
|
|
17
17
|
lastIteration?: string
|
|
18
18
|
logs: string[]
|
|
19
|
+
startTokens?: number
|
|
20
|
+
totalTokens: number
|
|
21
|
+
maxIterations: number
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
function journalPath(sessionId: string): string {
|
|
@@ -23,7 +26,11 @@ function journalPath(sessionId: string): string {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/** Create or reset an autonomous loop journal. */
|
|
26
|
-
export function createAutoloopJournal(
|
|
29
|
+
export function createAutoloopJournal(
|
|
30
|
+
sessionId: string,
|
|
31
|
+
prompt: string,
|
|
32
|
+
startTokens = 0,
|
|
33
|
+
): AutoloopJournal {
|
|
27
34
|
ensureDir()
|
|
28
35
|
const journal: AutoloopJournal = {
|
|
29
36
|
sessionId,
|
|
@@ -32,11 +39,22 @@ export function createAutoloopJournal(sessionId: string, prompt: string): Autolo
|
|
|
32
39
|
iterations: 0,
|
|
33
40
|
startedAt: new Date().toISOString(),
|
|
34
41
|
logs: [],
|
|
42
|
+
startTokens,
|
|
43
|
+
totalTokens: 0,
|
|
44
|
+
maxIterations: 100,
|
|
35
45
|
}
|
|
36
46
|
writeFileSync(journalPath(sessionId), JSON.stringify(journal, null, 2), 'utf-8')
|
|
37
47
|
return journal
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
/** Accumulate token usage into the loop journal. */
|
|
51
|
+
export function recordLoopTokens(sessionId: string, delta: number): void {
|
|
52
|
+
const journal = readAutoloopJournal(sessionId)
|
|
53
|
+
if (!journal) return
|
|
54
|
+
journal.totalTokens += delta
|
|
55
|
+
writeFileSync(journalPath(sessionId), JSON.stringify(journal, null, 2), 'utf-8')
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
/** Read the journal for an autonomous loop. */
|
|
41
59
|
export function readAutoloopJournal(sessionId: string): AutoloopJournal | null {
|
|
42
60
|
const path = journalPath(sessionId)
|
|
@@ -48,6 +66,19 @@ export function readAutoloopJournal(sessionId: string): AutoloopJournal | null {
|
|
|
48
66
|
}
|
|
49
67
|
}
|
|
50
68
|
|
|
69
|
+
/** End-of-loop-turn accounting: log the iteration, record the token delta, and
|
|
70
|
+
* stop the loop if it has reached its max-iteration guard (spec §七). */
|
|
71
|
+
export function recordLoopTurn(sessionId: string, summary: string, delta: number): void {
|
|
72
|
+
const journal = readAutoloopJournal(sessionId)
|
|
73
|
+
if (!journal || journal.status !== 'active') return
|
|
74
|
+
logAutoloopIteration(sessionId, summary)
|
|
75
|
+
recordLoopTokens(sessionId, delta)
|
|
76
|
+
const updated = readAutoloopJournal(sessionId)
|
|
77
|
+
if (updated && updated.iterations >= updated.maxIterations) {
|
|
78
|
+
completeAutoloopJournal(sessionId, 'stopped')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
/** Log an iteration in the autonomous loop journal. */
|
|
52
83
|
export function logAutoloopIteration(sessionId: string, summary: string): void {
|
|
53
84
|
const journal = readAutoloopJournal(sessionId)
|
|
@@ -85,6 +116,15 @@ export function listActiveAutoloops(): AutoloopJournal[] {
|
|
|
85
116
|
return journals
|
|
86
117
|
}
|
|
87
118
|
|
|
119
|
+
/** Format active loop journals as one-line rows for the /usage panel. */
|
|
120
|
+
export function formatLoopRows(journals: AutoloopJournal[]): string[] {
|
|
121
|
+
return journals.map((j) => {
|
|
122
|
+
const per = j.iterations > 0 ? Math.round(j.totalTokens / j.iterations) : 0
|
|
123
|
+
const last = j.lastIteration ? new Date(j.lastIteration).toLocaleString() : 'N/A'
|
|
124
|
+
return `${j.sessionId.slice(-8)} 🔄 ${j.iterations} iterations · ${j.totalTokens} tokens · ${per} /run · last ${last}`
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
88
128
|
/** Get the autonomous loop status message. */
|
|
89
129
|
export function getAutoloopStatus(sessionId: string): string {
|
|
90
130
|
const journal = readAutoloopJournal(sessionId)
|
package/src/config/defaults.ts
CHANGED
|
@@ -13,6 +13,9 @@ export const DEFAULT_CONFIG: MiphamConfig = {
|
|
|
13
13
|
defaultProvider: 'deepseek',
|
|
14
14
|
defaultModel: 'deepseek-v4-pro',
|
|
15
15
|
permission: 'default',
|
|
16
|
+
showThinking: 'off',
|
|
17
|
+
showSchedulingNotices: false,
|
|
18
|
+
showCommandPicker: false,
|
|
16
19
|
// Org 级权限限制(可选):forbiddenModes 禁指定模式 / maxAllowedMode 封顶层级;
|
|
17
20
|
// 请求被禁模式时 fail-closed 降级(如 forbiddenModes:['bypassPermissions'])。
|
|
18
21
|
// permissionRestrictions: { forbiddenModes: ['bypassPermissions'] },
|
package/src/core/engine.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { UsageTracker } from './usage-tracker'
|
|
|
38
38
|
import { getMetrics } from './metrics'
|
|
39
39
|
import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
|
|
40
40
|
import { getFileInboxTransport } from '../agent/cross-session/file-inbox'
|
|
41
|
+
import { registerWakeupHandler } from '../tools/scheduling/schedule-wakeup'
|
|
41
42
|
import { accumulateGraftSavings } from '../shared/graft-savings'
|
|
42
43
|
import { getMessageBus, type AgentMessage } from '../agent/message-bus'
|
|
43
44
|
import { createT } from '../i18n-core/t'
|
|
@@ -101,6 +102,12 @@ export class QueryEngine {
|
|
|
101
102
|
/** 注入的 LLM chat 缝。未设置时回退 this.registry(strangler-fig)。 */
|
|
102
103
|
private llm?: Llm
|
|
103
104
|
|
|
105
|
+
/** Pending /loop re-invocation prompts, enqueued by the ScheduleWakeup timer. */
|
|
106
|
+
private wakeupQueue: Array<{ prompt: string; noop: boolean }> = []
|
|
107
|
+
|
|
108
|
+
/** Callback fired whenever a wakeup is enqueued (the ScheduleWakeup timer fired). */
|
|
109
|
+
private onWakeupEnqueued: (() => void) | null = null
|
|
110
|
+
|
|
104
111
|
constructor(
|
|
105
112
|
private registry: ProviderRegistry,
|
|
106
113
|
private context: ContextManager,
|
|
@@ -109,6 +116,36 @@ export class QueryEngine {
|
|
|
109
116
|
ruleEngine?: ExperienceRuleEngine,
|
|
110
117
|
) {
|
|
111
118
|
this.ruleEngine = ruleEngine
|
|
119
|
+
// ScheduleWakeup 的 timer 到期后,把 loop prompt 交回本引擎 re-invoke。
|
|
120
|
+
registerWakeupHandler((_sessionId, prompt, noop) => this.enqueueWakeup(prompt, noop))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Enqueue a loop prompt for later re-invocation (keep-latest — drops older wakeups). */
|
|
124
|
+
enqueueWakeup(prompt: string, noop = false): void {
|
|
125
|
+
// 队列只保留最新(丢弃旧唤醒,防堆积)
|
|
126
|
+
this.wakeupQueue = [{ prompt, noop }]
|
|
127
|
+
this.onWakeupEnqueued?.()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Register a callback fired whenever a wakeup is enqueued (the ScheduleWakeup
|
|
131
|
+
* timer fired). The app subscribes to drain the queue while idle. */
|
|
132
|
+
setOnWakeupEnqueued(cb: (() => void) | null): void {
|
|
133
|
+
this.onWakeupEnqueued = cb
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Empty the wakeup queue (stops re-invocation at the max-iteration guard). */
|
|
137
|
+
clearWakeupQueue(): void {
|
|
138
|
+
this.wakeupQueue = []
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Pop the next pending wakeup prompt, or null when the queue is empty. */
|
|
142
|
+
dequeueWakeup(): { prompt: string; noop: boolean } | null {
|
|
143
|
+
return this.wakeupQueue.shift() ?? null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Whether a wakeup prompt is pending re-invocation. */
|
|
147
|
+
hasPendingWakeup(): boolean {
|
|
148
|
+
return this.wakeupQueue.length > 0
|
|
112
149
|
}
|
|
113
150
|
|
|
114
151
|
/** Session identifier for cross-session messaging. Set by the app startup via setSessionId(). */
|
|
@@ -486,8 +523,11 @@ export class QueryEngine {
|
|
|
486
523
|
yield chunk
|
|
487
524
|
|
|
488
525
|
if (chunk.type === 'error') {
|
|
526
|
+
// #23: client-generated errors are system lines, not model output —
|
|
527
|
+
// store as `system` so resume renders an error line (⚠) rather than
|
|
528
|
+
// attributing the failure to the assistant.
|
|
489
529
|
this.context.addMessage({
|
|
490
|
-
role: '
|
|
530
|
+
role: 'system',
|
|
491
531
|
content: t('errors.model_error', { error: chunk.error ?? 'Unknown error' }),
|
|
492
532
|
})
|
|
493
533
|
return
|
|
@@ -964,7 +1004,7 @@ export class QueryEngine {
|
|
|
964
1004
|
// P1-4: Increment consecutive block counter; if limit exceeded,
|
|
965
1005
|
// tell the model to move on instead of retrying.
|
|
966
1006
|
const limitExceeded = this.permission.incrementBlockCounter()
|
|
967
|
-
const baseError =
|
|
1007
|
+
const baseError = this.buildDenialError(name, tool, params)
|
|
968
1008
|
const moveOnHint = limitExceeded
|
|
969
1009
|
? '\n(Consecutive block limit reached. Please try a different approach or ask the user for guidance.)'
|
|
970
1010
|
: ''
|
|
@@ -1297,6 +1337,29 @@ export class QueryEngine {
|
|
|
1297
1337
|
return this.tools
|
|
1298
1338
|
}
|
|
1299
1339
|
|
|
1340
|
+
/**
|
|
1341
|
+
* Build a rich permission-denial error naming the mode (level), the setting
|
|
1342
|
+
* (rule/level) that caused the denial, and the correct fix (#52).
|
|
1343
|
+
*/
|
|
1344
|
+
private buildDenialError(
|
|
1345
|
+
name: string,
|
|
1346
|
+
tool: ToolDefinition,
|
|
1347
|
+
params: Record<string, unknown>,
|
|
1348
|
+
): string {
|
|
1349
|
+
const { reason, rulePattern } = this.permission.explainDenial(tool, params)
|
|
1350
|
+
const mode = this.permission.getMode()
|
|
1351
|
+
switch (reason) {
|
|
1352
|
+
case 'deny-rule':
|
|
1353
|
+
return t('errors.tool_denied_deny_rule', { name, pattern: rulePattern ?? '?' })
|
|
1354
|
+
case 'ask-rule':
|
|
1355
|
+
return t('errors.tool_denied_ask_rule', { name, pattern: rulePattern ?? '?' })
|
|
1356
|
+
default:
|
|
1357
|
+
// mode-baseline / tool-default / legacy-rule / system-default — a mode
|
|
1358
|
+
// switch (or /permissions) resolves it.
|
|
1359
|
+
return t('errors.tool_denied_mode', { name, mode })
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1300
1363
|
getRuleEngine(): ExperienceRuleEngine | undefined {
|
|
1301
1364
|
return this.ruleEngine
|
|
1302
1365
|
}
|
package/src/core/permission.ts
CHANGED
|
@@ -62,6 +62,18 @@ function isVerificationCommand(input: Record<string, unknown>): boolean {
|
|
|
62
62
|
|
|
63
63
|
const VALID_MODES: Set<string> = new Set<string>(MODE_CYCLE)
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Why a tool resolved to 'ask' — for rich denial errors (#52).
|
|
67
|
+
* Mirrors the resolution chain order in `check()` (first match wins).
|
|
68
|
+
*/
|
|
69
|
+
export type PermissionDenialReason =
|
|
70
|
+
| 'deny-rule' // org deny rule — overrides mode, cannot be switched away
|
|
71
|
+
| 'ask-rule' // explicit ask rule — still requires approval under any mode
|
|
72
|
+
| 'legacy-rule' // legacy exact-name rule (setRule)
|
|
73
|
+
| 'mode-baseline' // mode-specific default (acceptEdits/plan) → ask
|
|
74
|
+
| 'tool-default' // tool.permission === 'ask'
|
|
75
|
+
| 'system-default' // no rule, no tool permission → fallback ask
|
|
76
|
+
|
|
65
77
|
export class PermissionSystem {
|
|
66
78
|
private allowRules: PermissionRuleEntry[] = []
|
|
67
79
|
private denyRules: PermissionRuleEntry[] = []
|
|
@@ -344,6 +356,39 @@ export class PermissionSystem {
|
|
|
344
356
|
return this.check(tool, input) === 'bypass'
|
|
345
357
|
}
|
|
346
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Explain WHY a tool resolves to 'ask' (rich denial errors — #52).
|
|
361
|
+
*
|
|
362
|
+
* Only call after `check()` returns 'ask'; mirrors its resolution order
|
|
363
|
+
* (deny → ask → legacy → mode → tool → system). Returns the matched
|
|
364
|
+
* rule pattern when the denial came from a deny/ask rule.
|
|
365
|
+
*/
|
|
366
|
+
explainDenial(
|
|
367
|
+
tool: ToolDefinition,
|
|
368
|
+
input: Record<string, unknown>,
|
|
369
|
+
): { reason: PermissionDenialReason; rulePattern?: string } {
|
|
370
|
+
for (const rule of this.denyRules) {
|
|
371
|
+
if (this.ruleMatches(rule, tool, input)) {
|
|
372
|
+
return { reason: 'deny-rule', rulePattern: rule.pattern }
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const rule of this.askRules) {
|
|
376
|
+
if (this.ruleMatches(rule, tool, input)) {
|
|
377
|
+
return { reason: 'ask-rule', rulePattern: rule.pattern }
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (this.legacyRules.has(tool.name)) {
|
|
381
|
+
return { reason: 'legacy-rule' }
|
|
382
|
+
}
|
|
383
|
+
if (this.modeBaseline(tool, input) !== 'mode-baseline') {
|
|
384
|
+
return { reason: 'mode-baseline' }
|
|
385
|
+
}
|
|
386
|
+
if (tool.permission) {
|
|
387
|
+
return { reason: 'tool-default' }
|
|
388
|
+
}
|
|
389
|
+
return { reason: 'system-default' }
|
|
390
|
+
}
|
|
391
|
+
|
|
347
392
|
// ── Helpers ──
|
|
348
393
|
|
|
349
394
|
private ruleMatches(
|
|
@@ -364,6 +364,7 @@
|
|
|
364
364
|
"provider_line": "Provider: {provider}",
|
|
365
365
|
"model_line": "Model: {model}",
|
|
366
366
|
"tool_section": "── Per-Tool ──",
|
|
367
|
+
"loops_header": "── Autonomous Loops ──",
|
|
367
368
|
"hint": "Use /context for detailed stats, /compact to free space."
|
|
368
369
|
},
|
|
369
370
|
"reload_skills": {
|
|
@@ -940,6 +941,9 @@
|
|
|
940
941
|
},
|
|
941
942
|
"errors": {
|
|
942
943
|
"tool_not_allowed": "Tool \"{name}\" requires user approval (permission: ask). The tool was not executed. Press Shift+Tab to switch permission mode, or run /permissions.",
|
|
944
|
+
"tool_denied_deny_rule": "Tool \"{name}\" blocked by a deny rule (\"{pattern}\"). Deny rules override permission mode — try a different approach.",
|
|
945
|
+
"tool_denied_ask_rule": "Tool \"{name}\" requires approval (ask rule: \"{pattern}\"). Approve when prompted, or adjust the rule via /permissions.",
|
|
946
|
+
"tool_denied_mode": "Tool \"{name}\" requires approval under \"{mode}\" mode. Press Shift+Tab to switch permission mode, or run /permissions.",
|
|
943
947
|
"tool_blocked": "Tool \"{name}\" blocked by hook",
|
|
944
948
|
"user_input_blocked": "User input blocked by hook.",
|
|
945
949
|
"dlp_blocked": "Request blocked by DLP policy.",
|
|
@@ -364,6 +364,7 @@
|
|
|
364
364
|
"provider_line": "提供商: {provider}",
|
|
365
365
|
"model_line": "模型: {model}",
|
|
366
366
|
"tool_section": "── 按工具 ──",
|
|
367
|
+
"loops_header": "── 自动循环 ──",
|
|
367
368
|
"hint": "使用 /context 查看详细统计,/compact 释放空间。"
|
|
368
369
|
},
|
|
369
370
|
"reload_skills": {
|
|
@@ -940,6 +941,9 @@
|
|
|
940
941
|
},
|
|
941
942
|
"errors": {
|
|
942
943
|
"tool_not_allowed": "工具 \"{name}\" 需要用户批准(权限:ask),未执行。按 Shift+Tab 切换权限模式,或运行 /permissions。",
|
|
944
|
+
"tool_denied_deny_rule": "工具 \"{name}\" 被拒绝规则(\"{pattern}\")阻止。拒绝规则优先于权限模式 — 请改用其他方式。",
|
|
945
|
+
"tool_denied_ask_rule": "工具 \"{name}\" 需要批准(ask 规则:\"{pattern}\")。请在提示时批准,或通过 /permissions 调整规则。",
|
|
946
|
+
"tool_denied_mode": "工具 \"{name}\" 在 \"{mode}\" 模式下需要批准。按 Shift+Tab 切换权限模式,或运行 /permissions。",
|
|
943
947
|
"tool_blocked": "工具 \"{name}\" 被钩子拦截",
|
|
944
948
|
"user_input_blocked": "用户输入被钩子拦截。",
|
|
945
949
|
"dlp_blocked": "请求被 DLP 策略阻止。",
|
|
@@ -21,7 +21,7 @@ export class OpenAICompatProvider implements ProviderInstance {
|
|
|
21
21
|
model: req.model,
|
|
22
22
|
messages: this.convertMessages(req.messages, req.systemPrompt),
|
|
23
23
|
stream: true,
|
|
24
|
-
max_tokens: req.maxTokens,
|
|
24
|
+
max_tokens: req.maxTokens || 8192,
|
|
25
25
|
temperature: req.temperature,
|
|
26
26
|
tools: req.tools?.map((t) => ({ type: 'function', function: t })),
|
|
27
27
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export const PACKAGE_NAME = '@miphamai/cli' as const
|
|
10
10
|
|
|
11
11
|
/** 当前发布版本 */
|
|
12
|
-
export const PACKAGE_VERSION = '0.
|
|
12
|
+
export const PACKAGE_VERSION = '0.60.1' as const
|
|
13
13
|
|
|
14
14
|
/** npm install 全局安装命令 */
|
|
15
15
|
export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
|
package/src/shared/types.ts
CHANGED
|
@@ -135,6 +135,17 @@ export interface MiphamConfig {
|
|
|
135
135
|
permissionRestrictions?: PermissionRestrictions
|
|
136
136
|
/** User-defined permission rules (allow/deny patterns), wired into the runtime PermissionSystem. */
|
|
137
137
|
permissionRules?: { allow?: string[]; deny?: string[] }
|
|
138
|
+
/**
|
|
139
|
+
* How to render the model's reasoning/thinking before the answer:
|
|
140
|
+
* - `off` → hide entirely (default — clean output)
|
|
141
|
+
* - `minimal` → content-free "thinking…" indicator
|
|
142
|
+
* - `full` → last 200 chars of the actual thinking text
|
|
143
|
+
*/
|
|
144
|
+
showThinking?: 'off' | 'minimal' | 'full'
|
|
145
|
+
/** When false, hide the `⏰ Wakeup scheduled` scheduling confirmation notices. Default false. */
|
|
146
|
+
showSchedulingNotices?: boolean
|
|
147
|
+
/** When false, disable the slash-command picker auto-popup on `/`. Default false. */
|
|
148
|
+
showCommandPicker?: boolean
|
|
138
149
|
providers: ProviderConfig[]
|
|
139
150
|
skills?: { paths: string[]; mcpServers: McpServerConfig[] }
|
|
140
151
|
marketplace?: {
|
|
@@ -34,6 +34,12 @@ export function launchApp(appName: string): { success: boolean; message: string
|
|
|
34
34
|
}
|
|
35
35
|
return { success: true, message: `Launched: ${appName}` }
|
|
36
36
|
} catch (err) {
|
|
37
|
-
|
|
37
|
+
// #57: on macOS an `open -a` failure is often missing Automation permission —
|
|
38
|
+
// point the user at the fix instead of a raw error.
|
|
39
|
+
const macHint =
|
|
40
|
+
process.platform === 'darwin'
|
|
41
|
+
? ' On macOS, grant Automation permission (System Settings → Privacy & Security → Automation) if prompted.'
|
|
42
|
+
: ''
|
|
43
|
+
return { success: false, message: `Failed to launch ${appName}: ${String(err)}${macHint}` }
|
|
38
44
|
}
|
|
39
45
|
}
|
|
@@ -52,6 +52,12 @@ $bitmap.Dispose()
|
|
|
52
52
|
} catch {
|
|
53
53
|
// ignore cleanup failures
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
// #57: on macOS a screencapture failure is almost always missing Screen
|
|
56
|
+
// Recording permission — point the user at the fix instead of a raw error.
|
|
57
|
+
const macHint =
|
|
58
|
+
process.platform === 'darwin'
|
|
59
|
+
? '\n(macOS: grant Screen Recording permission in System Settings → Privacy & Security → Screen Recording)'
|
|
60
|
+
: ''
|
|
61
|
+
return { success: false, error: `Screenshot failed: ${String(err)}${macHint}` }
|
|
56
62
|
}
|
|
57
63
|
}
|
|
@@ -6,6 +6,15 @@ import type { ToolDefinition } from '../../shared/index.ts'
|
|
|
6
6
|
*/
|
|
7
7
|
const activeTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
8
8
|
|
|
9
|
+
let wakeupHandler: ((sessionId: string, prompt: string, noop?: boolean) => void) | null = null
|
|
10
|
+
|
|
11
|
+
/** 引擎在启动时注入——timer 到期后回调,把 loop prompt 交回引擎 re-invoke。 */
|
|
12
|
+
export function registerWakeupHandler(
|
|
13
|
+
fn: (sessionId: string, prompt: string, noop?: boolean) => void,
|
|
14
|
+
): void {
|
|
15
|
+
wakeupHandler = fn
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
export const scheduleWakeupTool: ToolDefinition = {
|
|
10
19
|
name: 'ScheduleWakeup',
|
|
11
20
|
description:
|
|
@@ -31,6 +40,10 @@ export const scheduleWakeupTool: ToolDefinition = {
|
|
|
31
40
|
type: 'boolean',
|
|
32
41
|
description: 'Set to true to end the dynamic loop immediately.',
|
|
33
42
|
},
|
|
43
|
+
noop: {
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
description: 'true = 本轮无事可报(仅用于 UI 折叠空闲提示,不改变 re-invoke 语义)',
|
|
46
|
+
},
|
|
34
47
|
},
|
|
35
48
|
required: [],
|
|
36
49
|
},
|
|
@@ -58,6 +71,7 @@ export const scheduleWakeupTool: ToolDefinition = {
|
|
|
58
71
|
const delaySeconds = params.delaySeconds as number
|
|
59
72
|
const prompt = (params.prompt as string) || ''
|
|
60
73
|
const reason = (params.reason as string) || 'scheduled wakeup'
|
|
74
|
+
const noop = params.noop === true
|
|
61
75
|
|
|
62
76
|
if (!delaySeconds || typeof delaySeconds !== 'number') {
|
|
63
77
|
return {
|
|
@@ -86,6 +100,7 @@ export const scheduleWakeupTool: ToolDefinition = {
|
|
|
86
100
|
const timerKey = `${sessionId}:wakeup`
|
|
87
101
|
const timeoutId = setTimeout(() => {
|
|
88
102
|
activeTimers.delete(timerKey)
|
|
103
|
+
wakeupHandler?.(sessionId, prompt, noop)
|
|
89
104
|
}, delaySeconds * 1000)
|
|
90
105
|
|
|
91
106
|
activeTimers.set(timerKey, timeoutId)
|