Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03c61bf591 | ||
|
|
f57aa39d53 | ||
|
|
41a318df66 | ||
|
|
e533a35109 | ||
|
|
934d4bcf32 | ||
|
|
91ae0cc67d | ||
|
|
7859f0dd2d | ||
|
|
e131491db4 | ||
|
|
08e2bb4034 | ||
|
|
04f33e584c | ||
|
|
8d76a57fe8 |
34
README.ko.md
34
README.ko.md
@@ -110,7 +110,7 @@ cat ~/.config/opencode/opencode.json # plugin 배열에 "oh-my-opencode"가 있
|
||||
|
||||
### 4단계: 인증정보 설정
|
||||
|
||||
plugin 에 oh my opencode 를 추가했다면, tmux 를 사용하여 사용자의 인증 설정을 도와주세요.
|
||||
plugin 에 oh my opencode 를 추가했다면, tmux 를 사용하여 사용자의 인증 설정을 도와주세요. **기본적으로 Google Auth (4.2단계) 설정을 권장합니다.**
|
||||
|
||||
#### 4.1 Anthropic (Claude)
|
||||
|
||||
@@ -123,7 +123,9 @@ opencode auth login
|
||||
|
||||
#### 4.2 Google Gemini (내장 Antigravity OAuth)
|
||||
|
||||
먼저 `~/.config/opencode/oh-my-opencode.json` (또는 프로젝트 레벨의 `.opencode/oh-my-opencode.json`)에서 Google auth를 활성화:
|
||||
**권장되는 기본 설정입니다.**
|
||||
|
||||
먼저 주도적으로 `~/.config/opencode/oh-my-opencode.json` (또는 프로젝트 레벨의 `.opencode/oh-my-opencode.json`)에서 Google auth를 활성화하세요:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -170,9 +172,9 @@ opencode auth login
|
||||
|
||||
### ⚠️ 주의
|
||||
|
||||
**사용자가 명시적으로 요청하지 않는 한, `oh-my-opencode.json` 설정 파일을 생성하거나 수정하지 마세요.**
|
||||
**사용자가 명시적으로 요청하지 않는 한, 모델 설정을 변경하거나 기능(에이전트, 훅, MCP)을 비활성화하지 마세요.**
|
||||
|
||||
플러그인은 설정 파일 없이도 기본값으로 완벽하게 동작합니다. 설정 파일은 사용자가 커스터마이징을 원할 때만 필요합니다.
|
||||
플러그인은 기본값으로 완벽하게 동작합니다. 권장 사항인 `google_auth: true` 설정을 제외하고는, 명시적인 요청 없이 다른 설정을 변경하거나 기능을 끄지 마세요.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -334,6 +336,18 @@ OpenCode 는 아주 확장가능하고 아주 커스터마이저블합니다.
|
||||
- **Empty Task Response Detector**: 서브 에이전트가 수행한 작업이 비어있거나 무의미한 응답을 반환하는 경우를 감지하여, 오류 없이 우아하게 처리합니다.
|
||||
- **Grep Output Truncator**: Grep 검색 결과가 너무 길어 컨텍스트를 장악해버리는 것을 방지하기 위해, 과도한 출력을 자동으로 자릅니다.
|
||||
|
||||
필요 없는 훅이 있다면, `~/.config/opencode/oh-my-opencode.json` 혹은 `.opencode/oh-my-opencode.json`의 `disabled_hooks`를 사용하여 비활성화할 수 있습니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_hooks": ["session-notification", "comment-checker"]
|
||||
}
|
||||
```
|
||||
|
||||
사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`
|
||||
|
||||
> **참고**: `disabled_hooks`는 Oh My OpenCode의 내장 훅을 제어합니다. Claude Code의 `settings.json` 훅을 비활성화하려면 `claude_code.hooks: false`를 대신 사용하세요 ([호환성 토글](#호환성-토글) 참고).
|
||||
|
||||
### Claude Code 호환성
|
||||
|
||||
Oh My OpenCode는 Claude Code 설정과 완벽하게 호환됩니다. Claude Code를 사용하셨다면, 기존 설정을 그대로 사용할 수 있습니다.
|
||||
@@ -441,6 +455,18 @@ Schema 자동 완성이 지원됩니다:
|
||||
}
|
||||
```
|
||||
|
||||
### Google Auth
|
||||
|
||||
Google Gemini 모델을 위한 내장 Antigravity OAuth를 활성화합니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"google_auth": true
|
||||
}
|
||||
```
|
||||
|
||||
활성화하면 `opencode auth login` 실행 시 Google 프로바이더에서 "OAuth with Google (Antigravity)" 로그인 옵션이 표시됩니다.
|
||||
|
||||
### Agents
|
||||
|
||||
내장 에이전트 설정을 오버라이드할 수 있습니다:
|
||||
|
||||
34
README.md
34
README.md
@@ -109,7 +109,7 @@ cat ~/.config/opencode/opencode.json # Should contain "oh-my-opencode" in plugi
|
||||
|
||||
### Step 4: Configure Authentication
|
||||
|
||||
Once `oh-my-opencode` is added to the plugin list, use `tmux` to assist the user with authentication setup.
|
||||
Once `oh-my-opencode` is added to the plugin list, use `tmux` to assist the user with authentication setup. **It is RECOMMENDED to set up Google Auth (Step 4.2) by default.**
|
||||
|
||||
#### 4.1 Anthropic (Claude)
|
||||
|
||||
@@ -122,7 +122,9 @@ opencode auth login
|
||||
|
||||
#### 4.2 Google Gemini (Built-in Antigravity OAuth)
|
||||
|
||||
First, enable Google auth in `~/.config/opencode/oh-my-opencode.json` (or `.opencode/oh-my-opencode.json` for project-level):
|
||||
**This is the RECOMMENDED default configuration.**
|
||||
|
||||
First, proactively enable Google auth in `~/.config/opencode/oh-my-opencode.json` (or `.opencode/oh-my-opencode.json` for project-level):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -169,9 +171,9 @@ For users who prefer proxy-based authentication, [VibeProxy](https://github.com/
|
||||
|
||||
### ⚠️ Warning
|
||||
|
||||
**Do NOT create or modify the `oh-my-opencode.json` configuration file unless the user explicitly requests it.**
|
||||
**Do NOT modify model settings or disable features (agents, hooks, MCPs) unless the user explicitly requests it.**
|
||||
|
||||
The plugin works perfectly with default settings without any configuration file. The configuration file is only needed when the user wants customization.
|
||||
The plugin works perfectly with default settings. While you SHOULD enable `google_auth: true` as recommended, do not alter other configurations or disable features without a specific request.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -332,6 +334,18 @@ Example workflow:
|
||||
- **Empty Task Response Detector**: Detects when subagent tasks return empty or meaningless responses and handles gracefully.
|
||||
- **Grep Output Truncator**: Prevents grep output from overwhelming the context by truncating excessively long results.
|
||||
|
||||
You can disable specific built-in hooks using `disabled_hooks` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_hooks": ["session-notification", "comment-checker"]
|
||||
}
|
||||
```
|
||||
|
||||
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`
|
||||
|
||||
> **Note**: `disabled_hooks` controls Oh My OpenCode's built-in hooks. To disable Claude Code's `settings.json` hooks, use `claude_code.hooks: false` instead (see [Compatibility Toggles](#compatibility-toggles)).
|
||||
|
||||
### Claude Code Compatibility
|
||||
|
||||
Oh My OpenCode provides seamless Claude Code configuration compatibility. If you've been using Claude Code, your existing setup works out of the box.
|
||||
@@ -439,6 +453,18 @@ Schema autocomplete is supported:
|
||||
}
|
||||
```
|
||||
|
||||
### Google Auth
|
||||
|
||||
Enable built-in Antigravity OAuth for Google Gemini models:
|
||||
|
||||
```json
|
||||
{
|
||||
"google_auth": true
|
||||
}
|
||||
```
|
||||
|
||||
When enabled, `opencode auth login` will show "OAuth with Google (Antigravity)" as a login option for the Google provider.
|
||||
|
||||
### Agents
|
||||
|
||||
Override built-in agent settings:
|
||||
|
||||
@@ -31,6 +31,29 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"disabled_hooks": {
|
||||
"type": "array",
|
||||
"description": "List of built-in hooks to disable. Useful for selectively disabling hooks that may conflict with your workflow.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"todo-continuation-enforcer",
|
||||
"context-window-monitor",
|
||||
"session-recovery",
|
||||
"session-notification",
|
||||
"comment-checker",
|
||||
"grep-output-truncator",
|
||||
"directory-agents-injector",
|
||||
"directory-readme-injector",
|
||||
"empty-task-response-detector",
|
||||
"think-mode",
|
||||
"anthropic-auto-compact",
|
||||
"rules-injector",
|
||||
"background-notification",
|
||||
"auto-update-checker"
|
||||
]
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
@@ -155,6 +178,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"google_auth": {
|
||||
"type": "boolean",
|
||||
"description": "Enable built-in Antigravity OAuth for Google Gemini models. When true, adds 'OAuth with Google (Antigravity)' login option.",
|
||||
"default": false
|
||||
},
|
||||
"lsp": {
|
||||
"type": "object",
|
||||
"description": "Additional LSP server configurations specific to Oh My OpenCode.",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Command and arguments to start the LSP server"
|
||||
},
|
||||
"extensions": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "File extensions this server handles (e.g., [\".ts\", \".tsx\"])"
|
||||
},
|
||||
"priority": {
|
||||
"type": "number",
|
||||
"description": "Server priority (higher = preferred)"
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" },
|
||||
"description": "Environment variables for the LSP server"
|
||||
},
|
||||
"initialization": {
|
||||
"type": "object",
|
||||
"description": "Custom initialization options"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this LSP server"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"claude_code": {
|
||||
"type": "object",
|
||||
"description": "Toggle Claude Code compatibility features on/off. All default to true (enabled).",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "0.4.2",
|
||||
"version": "1.0.0",
|
||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -70,6 +70,21 @@ function isRetryableError(status: number): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
const GCP_PERMISSION_ERROR_PATTERNS = [
|
||||
"PERMISSION_DENIED",
|
||||
"does not have permission",
|
||||
"Cloud AI Companion API has not been used",
|
||||
"has not been enabled",
|
||||
] as const
|
||||
|
||||
function isGcpPermissionError(text: string): boolean {
|
||||
return GCP_PERMISSION_ERROR_PATTERNS.some((pattern) => text.includes(pattern))
|
||||
}
|
||||
|
||||
function calculateRetryDelay(attempt: number): number {
|
||||
return Math.min(200 * Math.pow(2, attempt), 2000)
|
||||
}
|
||||
|
||||
async function isRetryableResponse(response: Response): Promise<boolean> {
|
||||
if (isRetryableError(response.status)) return true
|
||||
if (response.status === 403) {
|
||||
@@ -155,23 +170,43 @@ async function attemptFetch(
|
||||
|
||||
debugLog(`[REQ] streaming=${transformed.streaming}, url=${transformed.url}`)
|
||||
|
||||
const response = await fetch(transformed.url, {
|
||||
method: init.method || "POST",
|
||||
headers: transformed.headers,
|
||||
body: JSON.stringify(transformed.body),
|
||||
signal: init.signal,
|
||||
})
|
||||
const maxPermissionRetries = 10
|
||||
for (let attempt = 0; attempt <= maxPermissionRetries; attempt++) {
|
||||
const response = await fetch(transformed.url, {
|
||||
method: init.method || "POST",
|
||||
headers: transformed.headers,
|
||||
body: JSON.stringify(transformed.body),
|
||||
signal: init.signal,
|
||||
})
|
||||
|
||||
debugLog(
|
||||
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
|
||||
)
|
||||
debugLog(
|
||||
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
|
||||
)
|
||||
|
||||
if (!response.ok && (await isRetryableResponse(response))) {
|
||||
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
|
||||
return null
|
||||
if (response.status === 403) {
|
||||
try {
|
||||
const text = await response.clone().text()
|
||||
if (isGcpPermissionError(text)) {
|
||||
if (attempt < maxPermissionRetries) {
|
||||
const delay = calculateRetryDelay(attempt)
|
||||
debugLog(`[RETRY] GCP permission error, retry ${attempt + 1}/${maxPermissionRetries} after ${delay}ms`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
continue
|
||||
}
|
||||
debugLog(`[RETRY] GCP permission error, max retries exceeded`)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!response.ok && (await isRetryableResponse(response))) {
|
||||
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
|
||||
return null
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
return response
|
||||
return null
|
||||
} catch (error) {
|
||||
debugLog(
|
||||
`Endpoint failed: ${endpoint} (${error instanceof Error ? error.message : "Unknown error"}), trying next`
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
AgentOverridesSchema,
|
||||
McpNameSchema,
|
||||
AgentNameSchema,
|
||||
HookNameSchema,
|
||||
} from "./schema"
|
||||
|
||||
export type {
|
||||
@@ -12,4 +13,5 @@ export type {
|
||||
AgentOverrides,
|
||||
McpName,
|
||||
AgentName,
|
||||
HookName,
|
||||
} from "./schema"
|
||||
|
||||
@@ -24,6 +24,24 @@ export const AgentNameSchema = z.enum([
|
||||
"document-writer",
|
||||
])
|
||||
|
||||
export const HookNameSchema = z.enum([
|
||||
"todo-continuation-enforcer",
|
||||
"context-window-monitor",
|
||||
"session-recovery",
|
||||
"session-notification",
|
||||
"comment-checker",
|
||||
"grep-output-truncator",
|
||||
"directory-agents-injector",
|
||||
"directory-readme-injector",
|
||||
"empty-task-response-detector",
|
||||
"think-mode",
|
||||
"anthropic-auto-compact",
|
||||
"rules-injector",
|
||||
"background-notification",
|
||||
"auto-update-checker",
|
||||
"ultrawork-mode",
|
||||
])
|
||||
|
||||
export const AgentOverrideConfigSchema = z.object({
|
||||
model: z.string().optional(),
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
@@ -62,6 +80,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
|
||||
$schema: z.string().optional(),
|
||||
disabled_mcps: z.array(McpNameSchema).optional(),
|
||||
disabled_agents: z.array(AgentNameSchema).optional(),
|
||||
disabled_hooks: z.array(HookNameSchema).optional(),
|
||||
agents: AgentOverridesSchema.optional(),
|
||||
claude_code: ClaudeCodeConfigSchema.optional(),
|
||||
google_auth: z.boolean().optional(),
|
||||
@@ -71,5 +90,6 @@ export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
|
||||
export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
|
||||
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
|
||||
export type AgentName = z.infer<typeof AgentNameSchema>
|
||||
export type HookName = z.infer<typeof HookNameSchema>
|
||||
|
||||
export { McpNameSchema, type McpName } from "../mcp/types"
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
LaunchInput,
|
||||
} from "./types"
|
||||
import { log } from "../../shared/logger"
|
||||
import { getMainSessionID } from "../claude-code-session-state"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
|
||||
@@ -59,6 +58,7 @@ export class BackgroundManager {
|
||||
parentSessionID: input.parentSessionID,
|
||||
parentMessageID: input.parentMessageID,
|
||||
description: input.description,
|
||||
prompt: input.prompt,
|
||||
agent: input.agent,
|
||||
status: "running",
|
||||
startedAt: new Date(),
|
||||
@@ -239,25 +239,19 @@ export class BackgroundManager {
|
||||
|
||||
const message = `[BACKGROUND TASK COMPLETED] Task "${task.description}" finished in ${duration}. Use background_output with task_id="${task.id}" to get results.`
|
||||
|
||||
const mainSessionID = getMainSessionID()
|
||||
if (!mainSessionID) {
|
||||
log("[background-agent] No main session ID available, relying on pending queue")
|
||||
return
|
||||
}
|
||||
|
||||
log("[background-agent] Sending notification to main session:", mainSessionID)
|
||||
log("[background-agent] Sending notification to parent session:", { parentSessionID: task.parentSessionID })
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.client.session.prompt({
|
||||
path: { id: mainSessionID },
|
||||
path: { id: task.parentSessionID },
|
||||
body: {
|
||||
parts: [{ type: "text", text: message }],
|
||||
},
|
||||
query: { directory: this.directory },
|
||||
})
|
||||
this.clearNotificationsForTask(task.id)
|
||||
log("[background-agent] Successfully sent prompt to main session")
|
||||
log("[background-agent] Successfully sent prompt to parent session:", { parentSessionID: task.parentSessionID })
|
||||
} catch (error) {
|
||||
log("[background-agent] prompt failed:", String(error))
|
||||
}
|
||||
@@ -316,7 +310,7 @@ export class BackgroundManager {
|
||||
if (!messagesResult.error && messagesResult.data) {
|
||||
const messages = messagesResult.data as Array<{
|
||||
info?: { role?: string }
|
||||
parts?: Array<{ type?: string; tool?: string; name?: string }>
|
||||
parts?: Array<{ type?: string; tool?: string; name?: string; text?: string }>
|
||||
}>
|
||||
const assistantMsgs = messages.filter(
|
||||
(m) => m.info?.role === "assistant"
|
||||
@@ -324,6 +318,7 @@ export class BackgroundManager {
|
||||
|
||||
let toolCalls = 0
|
||||
let lastTool: string | undefined
|
||||
let lastMessage: string | undefined
|
||||
|
||||
for (const msg of assistantMsgs) {
|
||||
const parts = msg.parts ?? []
|
||||
@@ -332,6 +327,9 @@ export class BackgroundManager {
|
||||
toolCalls++
|
||||
lastTool = part.tool || part.name || "unknown"
|
||||
}
|
||||
if (part.type === "text" && part.text) {
|
||||
lastMessage = part.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +339,10 @@ export class BackgroundManager {
|
||||
task.progress.toolCalls = toolCalls
|
||||
task.progress.lastTool = lastTool
|
||||
task.progress.lastUpdate = new Date()
|
||||
if (lastMessage) {
|
||||
task.progress.lastMessage = lastMessage
|
||||
task.progress.lastMessageAt = new Date()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log("[background-agent] Poll error for task:", { taskId: task.id, error })
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface TaskProgress {
|
||||
toolCalls: number
|
||||
lastTool?: string
|
||||
lastUpdate: Date
|
||||
lastMessage?: string
|
||||
lastMessageAt?: Date
|
||||
}
|
||||
|
||||
export interface BackgroundTask {
|
||||
@@ -16,6 +18,7 @@ export interface BackgroundTask {
|
||||
parentSessionID: string
|
||||
parentMessageID: string
|
||||
description: string
|
||||
prompt: string
|
||||
agent: string
|
||||
status: BackgroundTaskStatus
|
||||
startedAt: Date
|
||||
|
||||
@@ -13,3 +13,4 @@ export { createClaudeCodeHooksHook } from "./claude-code-hooks";
|
||||
export { createRulesInjectorHook } from "./rules-injector";
|
||||
export { createBackgroundNotificationHook } from "./background-notification"
|
||||
export { createAutoUpdateCheckerHook } from "./auto-update-checker";
|
||||
export { createUltraworkModeHook } from "./ultrawork-mode";
|
||||
|
||||
@@ -17,6 +17,8 @@ interface SessionNotificationConfig {
|
||||
idleConfirmationDelay?: number
|
||||
/** Skip notification if there are incomplete todos (default: true) */
|
||||
skipIfIncompleteTodos?: boolean
|
||||
/** Maximum number of sessions to track before cleanup (default: 100) */
|
||||
maxTrackedSessions?: number
|
||||
}
|
||||
|
||||
type Platform = "darwin" | "linux" | "win32" | "unsupported"
|
||||
@@ -103,12 +105,31 @@ export function createSessionNotification(
|
||||
soundPath: defaultSoundPath,
|
||||
idleConfirmationDelay: 1500,
|
||||
skipIfIncompleteTodos: true,
|
||||
maxTrackedSessions: 100,
|
||||
...config,
|
||||
}
|
||||
|
||||
const notifiedSessions = new Set<string>()
|
||||
const pendingTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
const sessionActivitySinceIdle = new Set<string>()
|
||||
// Track notification execution version to handle race conditions
|
||||
const notificationVersions = new Map<string, number>()
|
||||
|
||||
function cleanupOldSessions() {
|
||||
const maxSessions = mergedConfig.maxTrackedSessions
|
||||
if (notifiedSessions.size > maxSessions) {
|
||||
const sessionsToRemove = Array.from(notifiedSessions).slice(0, notifiedSessions.size - maxSessions)
|
||||
sessionsToRemove.forEach(id => notifiedSessions.delete(id))
|
||||
}
|
||||
if (sessionActivitySinceIdle.size > maxSessions) {
|
||||
const sessionsToRemove = Array.from(sessionActivitySinceIdle).slice(0, sessionActivitySinceIdle.size - maxSessions)
|
||||
sessionsToRemove.forEach(id => sessionActivitySinceIdle.delete(id))
|
||||
}
|
||||
if (notificationVersions.size > maxSessions) {
|
||||
const sessionsToRemove = Array.from(notificationVersions.keys()).slice(0, notificationVersions.size - maxSessions)
|
||||
sessionsToRemove.forEach(id => notificationVersions.delete(id))
|
||||
}
|
||||
}
|
||||
|
||||
function cancelPendingNotification(sessionID: string) {
|
||||
const timer = pendingTimers.get(sessionID)
|
||||
@@ -117,6 +138,8 @@ export function createSessionNotification(
|
||||
pendingTimers.delete(sessionID)
|
||||
}
|
||||
sessionActivitySinceIdle.add(sessionID)
|
||||
// Increment version to invalidate any in-flight notifications
|
||||
notificationVersions.set(sessionID, (notificationVersions.get(sessionID) ?? 0) + 1)
|
||||
}
|
||||
|
||||
function markSessionActivity(sessionID: string) {
|
||||
@@ -124,9 +147,14 @@ export function createSessionNotification(
|
||||
notifiedSessions.delete(sessionID)
|
||||
}
|
||||
|
||||
async function executeNotification(sessionID: string) {
|
||||
async function executeNotification(sessionID: string, version: number) {
|
||||
pendingTimers.delete(sessionID)
|
||||
|
||||
// Race condition fix: check if version matches (activity happened during async wait)
|
||||
if (notificationVersions.get(sessionID) !== version) {
|
||||
return
|
||||
}
|
||||
|
||||
if (sessionActivitySinceIdle.has(sessionID)) {
|
||||
sessionActivitySinceIdle.delete(sessionID)
|
||||
return
|
||||
@@ -136,9 +164,17 @@ export function createSessionNotification(
|
||||
|
||||
if (mergedConfig.skipIfIncompleteTodos) {
|
||||
const hasPendingWork = await hasIncompleteTodos(ctx, sessionID)
|
||||
// Re-check version after async call (race condition fix)
|
||||
if (notificationVersions.get(sessionID) !== version) {
|
||||
return
|
||||
}
|
||||
if (hasPendingWork) return
|
||||
}
|
||||
|
||||
if (notificationVersions.get(sessionID) !== version) {
|
||||
return
|
||||
}
|
||||
|
||||
notifiedSessions.add(sessionID)
|
||||
|
||||
try {
|
||||
@@ -172,20 +208,34 @@ export function createSessionNotification(
|
||||
if (pendingTimers.has(sessionID)) return
|
||||
|
||||
sessionActivitySinceIdle.delete(sessionID)
|
||||
|
||||
const currentVersion = (notificationVersions.get(sessionID) ?? 0) + 1
|
||||
notificationVersions.set(sessionID, currentVersion)
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
executeNotification(sessionID)
|
||||
executeNotification(sessionID, currentVersion)
|
||||
}, mergedConfig.idleConfirmationDelay)
|
||||
|
||||
pendingTimers.set(sessionID, timer)
|
||||
cleanupOldSessions()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "message.updated") {
|
||||
if (event.type === "message.updated" || event.type === "message.created") {
|
||||
const info = props?.info as Record<string, unknown> | undefined
|
||||
const sessionID = info?.sessionID as string | undefined
|
||||
if (sessionID) {
|
||||
markSessionActivity(sessionID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "tool.execute.before" || event.type === "tool.execute.after") {
|
||||
const sessionID = props?.sessionID as string | undefined
|
||||
if (sessionID) {
|
||||
markSessionActivity(sessionID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "session.deleted") {
|
||||
@@ -194,6 +244,7 @@ export function createSessionNotification(
|
||||
cancelPendingNotification(sessionInfo.id)
|
||||
notifiedSessions.delete(sessionInfo.id)
|
||||
sessionActivitySinceIdle.delete(sessionInfo.id)
|
||||
notificationVersions.delete(sessionInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
src/hooks/ultrawork-mode/constants.ts
Normal file
48
src/hooks/ultrawork-mode/constants.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/** Keyword patterns - "ultrawork", "ulw" (case-insensitive, word boundary) */
|
||||
export const ULTRAWORK_PATTERNS = [/\bultrawork\b/i, /\bulw\b/i]
|
||||
|
||||
/** Code block pattern to exclude from keyword detection */
|
||||
export const CODE_BLOCK_PATTERN = /```[\s\S]*?```/g
|
||||
|
||||
/** Inline code pattern to exclude */
|
||||
export const INLINE_CODE_PATTERN = /`[^`]+`/g
|
||||
|
||||
/**
|
||||
* ULTRAWORK_CONTEXT - Agent-Agnostic Guidance
|
||||
*
|
||||
* Key principles:
|
||||
* - NO specific agent names (oracle, librarian, etc.)
|
||||
* - Only provide guidance based on agent role/capability
|
||||
* - Emphasize parallel execution, TODO tracking, delegation
|
||||
*/
|
||||
export const ULTRAWORK_CONTEXT = `<ultrawork-mode>
|
||||
[CODE RED] Maximum precision required. Ultrathink before acting.
|
||||
|
||||
YOU MUST LEVERAGE ALL AVAILABLE AGENTS TO THEIR FULLEST POTENTIAL.
|
||||
TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
|
||||
|
||||
## AGENT UTILIZATION PRINCIPLES (by capability, not by name)
|
||||
- **Codebase Exploration**: Spawn exploration agents using BACKGROUND TASKS for file patterns, internal implementations, project structure
|
||||
- **Documentation & References**: Use librarian-type agents via BACKGROUND TASKS for API references, examples, external library docs
|
||||
- **Planning & Strategy**: NEVER plan yourself - ALWAYS spawn a dedicated planning agent for work breakdown
|
||||
- **High-IQ Reasoning**: Leverage specialized agents for architecture decisions, code review, strategic planning
|
||||
- **Frontend/UI Tasks**: Delegate to UI-specialized agents for design and implementation
|
||||
|
||||
## EXECUTION RULES
|
||||
- **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each.
|
||||
- **PARALLEL**: Fire independent agent calls simultaneously via background_task - NEVER wait sequentially.
|
||||
- **BACKGROUND FIRST**: Use background_task for exploration/research agents (10+ concurrent if needed).
|
||||
- **VERIFY**: Re-read request after completion. Check ALL requirements met before reporting done.
|
||||
- **DELEGATE**: Don't do everything yourself - orchestrate specialized agents for their strengths.
|
||||
|
||||
## WORKFLOW
|
||||
1. Analyze the request and identify required capabilities
|
||||
2. Spawn exploration/librarian agents via background_task in PARALLEL (10+ if needed)
|
||||
3. Use planning agents to create detailed work breakdown
|
||||
4. Execute with continuous verification against original requirements
|
||||
|
||||
</ultrawork-mode>
|
||||
|
||||
---
|
||||
|
||||
`
|
||||
33
src/hooks/ultrawork-mode/detector.ts
Normal file
33
src/hooks/ultrawork-mode/detector.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
ULTRAWORK_PATTERNS,
|
||||
CODE_BLOCK_PATTERN,
|
||||
INLINE_CODE_PATTERN,
|
||||
} from "./constants"
|
||||
|
||||
/**
|
||||
* Remove code blocks and inline code from text.
|
||||
* Prevents false positives when keywords appear in code.
|
||||
*/
|
||||
export function removeCodeBlocks(text: string): string {
|
||||
return text.replace(CODE_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect ultrawork keywords in text (excluding code blocks).
|
||||
*/
|
||||
export function detectUltraworkKeyword(text: string): boolean {
|
||||
const textWithoutCode = removeCodeBlocks(text)
|
||||
return ULTRAWORK_PATTERNS.some((pattern) => pattern.test(textWithoutCode))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from message parts.
|
||||
*/
|
||||
export function extractPromptText(
|
||||
parts: Array<{ type: string; text?: string }>
|
||||
): string {
|
||||
return parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text || "")
|
||||
.join("")
|
||||
}
|
||||
84
src/hooks/ultrawork-mode/index.ts
Normal file
84
src/hooks/ultrawork-mode/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { detectUltraworkKeyword, extractPromptText } from "./detector"
|
||||
import { ULTRAWORK_CONTEXT } from "./constants"
|
||||
import type { UltraworkModeState } from "./types"
|
||||
import { log } from "../../shared"
|
||||
|
||||
export * from "./detector"
|
||||
export * from "./constants"
|
||||
export * from "./types"
|
||||
|
||||
const ultraworkModeState = new Map<string, UltraworkModeState>()
|
||||
|
||||
export function clearUltraworkModeState(sessionID: string): void {
|
||||
ultraworkModeState.delete(sessionID)
|
||||
}
|
||||
|
||||
export function createUltraworkModeHook() {
|
||||
return {
|
||||
/**
|
||||
* chat.message hook - detect ultrawork/ulw keywords, inject context
|
||||
*
|
||||
* Execution timing: AFTER claudeCodeHooks["chat.message"]
|
||||
* Behavior:
|
||||
* 1. Extract text from user prompt
|
||||
* 2. Detect ultrawork/ulw keywords (excluding code blocks)
|
||||
* 3. If detected, prepend ULTRAWORK_CONTEXT to first text part
|
||||
*/
|
||||
"chat.message": async (
|
||||
input: {
|
||||
sessionID: string
|
||||
agent?: string
|
||||
model?: { providerID: string; modelID: string }
|
||||
messageID?: string
|
||||
},
|
||||
output: {
|
||||
message: Record<string, unknown>
|
||||
parts: Array<{ type: string; text?: string; [key: string]: unknown }>
|
||||
}
|
||||
): Promise<void> => {
|
||||
const state: UltraworkModeState = {
|
||||
detected: false,
|
||||
injected: false,
|
||||
}
|
||||
|
||||
const promptText = extractPromptText(output.parts)
|
||||
|
||||
if (!detectUltraworkKeyword(promptText)) {
|
||||
ultraworkModeState.set(input.sessionID, state)
|
||||
return
|
||||
}
|
||||
|
||||
state.detected = true
|
||||
log("Ultrawork keyword detected", { sessionID: input.sessionID })
|
||||
|
||||
const parts = output.parts as Array<{ type: string; text?: string }>
|
||||
const idx = parts.findIndex((p) => p.type === "text" && p.text)
|
||||
|
||||
if (idx >= 0) {
|
||||
parts[idx].text = `${ULTRAWORK_CONTEXT}${parts[idx].text ?? ""}`
|
||||
state.injected = true
|
||||
log("Ultrawork context injected", { sessionID: input.sessionID })
|
||||
}
|
||||
|
||||
ultraworkModeState.set(input.sessionID, state)
|
||||
},
|
||||
|
||||
/**
|
||||
* event hook - cleanup session state on deletion
|
||||
*/
|
||||
event: async ({
|
||||
event,
|
||||
}: {
|
||||
event: { type: string; properties?: unknown }
|
||||
}) => {
|
||||
if (event.type === "session.deleted") {
|
||||
const props = event.properties as
|
||||
| { info?: { id?: string } }
|
||||
| undefined
|
||||
if (props?.info?.id) {
|
||||
ultraworkModeState.delete(props.info.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
20
src/hooks/ultrawork-mode/types.ts
Normal file
20
src/hooks/ultrawork-mode/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface UltraworkModeState {
|
||||
/** Whether ultrawork keyword was detected */
|
||||
detected: boolean
|
||||
/** Whether context was injected */
|
||||
injected: boolean
|
||||
}
|
||||
|
||||
export interface ModelRef {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
|
||||
export interface MessageWithModel {
|
||||
model?: ModelRef
|
||||
}
|
||||
|
||||
export interface UltraworkModeInput {
|
||||
parts: Array<{ type: string; text?: string }>
|
||||
message: MessageWithModel
|
||||
}
|
||||
117
src/index.ts
117
src/index.ts
@@ -4,6 +4,7 @@ import {
|
||||
createTodoContinuationEnforcer,
|
||||
createContextWindowMonitorHook,
|
||||
createSessionRecoveryHook,
|
||||
createSessionNotification,
|
||||
createCommentCheckerHooks,
|
||||
createGrepOutputTruncatorHook,
|
||||
createDirectoryAgentsInjectorHook,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
createRulesInjectorHook,
|
||||
createBackgroundNotificationHook,
|
||||
createAutoUpdateCheckerHook,
|
||||
createUltraworkModeHook,
|
||||
} from "./hooks";
|
||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||
import {
|
||||
@@ -42,7 +44,7 @@ import { updateTerminalTitle } from "./features/terminal";
|
||||
import { builtinTools, createCallOmoAgent, createBackgroundTools } from "./tools";
|
||||
import { BackgroundManager } from "./features/background-agent";
|
||||
import { createBuiltinMcps } from "./mcp";
|
||||
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
|
||||
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig, type HookName } from "./config";
|
||||
import { log, deepMerge } from "./shared";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
@@ -103,6 +105,12 @@ function mergeConfigs(
|
||||
...(override.disabled_mcps ?? []),
|
||||
]),
|
||||
],
|
||||
disabled_hooks: [
|
||||
...new Set([
|
||||
...(base.disabled_hooks ?? []),
|
||||
...(override.disabled_hooks ?? []),
|
||||
]),
|
||||
],
|
||||
claude_code: deepMerge(base.claude_code, override.claude_code),
|
||||
};
|
||||
}
|
||||
@@ -135,6 +143,7 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
|
||||
agents: config.agents,
|
||||
disabled_agents: config.disabled_agents,
|
||||
disabled_mcps: config.disabled_mcps,
|
||||
disabled_hooks: config.disabled_hooks,
|
||||
claude_code: config.claude_code,
|
||||
});
|
||||
return config;
|
||||
@@ -142,34 +151,70 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
|
||||
|
||||
const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const pluginConfig = loadPluginConfig(ctx.directory);
|
||||
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
||||
const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName);
|
||||
|
||||
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
|
||||
const contextWindowMonitor = createContextWindowMonitorHook(ctx);
|
||||
const sessionRecovery = createSessionRecoveryHook(ctx);
|
||||
const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer")
|
||||
? createTodoContinuationEnforcer(ctx)
|
||||
: null;
|
||||
const contextWindowMonitor = isHookEnabled("context-window-monitor")
|
||||
? createContextWindowMonitorHook(ctx)
|
||||
: null;
|
||||
const sessionRecovery = isHookEnabled("session-recovery")
|
||||
? createSessionRecoveryHook(ctx)
|
||||
: null;
|
||||
const sessionNotification = isHookEnabled("session-notification")
|
||||
? createSessionNotification(ctx)
|
||||
: null;
|
||||
|
||||
// Wire up recovery state tracking between session-recovery and todo-continuation-enforcer
|
||||
// This prevents the continuation enforcer from injecting prompts during active recovery
|
||||
sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
|
||||
sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
|
||||
if (sessionRecovery && todoContinuationEnforcer) {
|
||||
sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
|
||||
sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
|
||||
}
|
||||
|
||||
const commentChecker = createCommentCheckerHooks();
|
||||
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
|
||||
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
|
||||
const directoryReadmeInjector = createDirectoryReadmeInjectorHook(ctx);
|
||||
const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx);
|
||||
const thinkMode = createThinkModeHook();
|
||||
const commentChecker = isHookEnabled("comment-checker")
|
||||
? createCommentCheckerHooks()
|
||||
: null;
|
||||
const grepOutputTruncator = isHookEnabled("grep-output-truncator")
|
||||
? createGrepOutputTruncatorHook(ctx)
|
||||
: null;
|
||||
const directoryAgentsInjector = isHookEnabled("directory-agents-injector")
|
||||
? createDirectoryAgentsInjectorHook(ctx)
|
||||
: null;
|
||||
const directoryReadmeInjector = isHookEnabled("directory-readme-injector")
|
||||
? createDirectoryReadmeInjectorHook(ctx)
|
||||
: null;
|
||||
const emptyTaskResponseDetector = isHookEnabled("empty-task-response-detector")
|
||||
? createEmptyTaskResponseDetectorHook(ctx)
|
||||
: null;
|
||||
const thinkMode = isHookEnabled("think-mode")
|
||||
? createThinkModeHook()
|
||||
: null;
|
||||
const claudeCodeHooks = createClaudeCodeHooksHook(ctx, {
|
||||
disabledHooks: (pluginConfig.claude_code?.hooks ?? true) ? undefined : true,
|
||||
});
|
||||
const anthropicAutoCompact = createAnthropicAutoCompactHook(ctx);
|
||||
const rulesInjector = createRulesInjectorHook(ctx);
|
||||
const autoUpdateChecker = createAutoUpdateCheckerHook(ctx);
|
||||
const anthropicAutoCompact = isHookEnabled("anthropic-auto-compact")
|
||||
? createAnthropicAutoCompactHook(ctx)
|
||||
: null;
|
||||
const rulesInjector = isHookEnabled("rules-injector")
|
||||
? createRulesInjectorHook(ctx)
|
||||
: null;
|
||||
const autoUpdateChecker = isHookEnabled("auto-update-checker")
|
||||
? createAutoUpdateCheckerHook(ctx)
|
||||
: null;
|
||||
const ultraworkMode = isHookEnabled("ultrawork-mode")
|
||||
? createUltraworkModeHook()
|
||||
: null;
|
||||
|
||||
updateTerminalTitle({ sessionId: "main" });
|
||||
|
||||
const backgroundManager = new BackgroundManager(ctx);
|
||||
|
||||
const backgroundNotificationHook = createBackgroundNotificationHook(backgroundManager);
|
||||
const backgroundNotificationHook = isHookEnabled("background-notification")
|
||||
? createBackgroundNotificationHook(backgroundManager)
|
||||
: null;
|
||||
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
|
||||
|
||||
const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
|
||||
@@ -189,6 +234,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
"chat.message": async (input, output) => {
|
||||
await claudeCodeHooks["chat.message"]?.(input, output);
|
||||
await ultraworkMode?.["chat.message"]?.(input, output);
|
||||
},
|
||||
|
||||
config: async (config) => {
|
||||
@@ -252,16 +298,18 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
},
|
||||
|
||||
event: async (input) => {
|
||||
await autoUpdateChecker.event(input);
|
||||
await autoUpdateChecker?.event(input);
|
||||
await claudeCodeHooks.event(input);
|
||||
await backgroundNotificationHook.event(input);
|
||||
await todoContinuationEnforcer.handler(input);
|
||||
await contextWindowMonitor.event(input);
|
||||
await directoryAgentsInjector.event(input);
|
||||
await directoryReadmeInjector.event(input);
|
||||
await rulesInjector.event(input);
|
||||
await thinkMode.event(input);
|
||||
await anthropicAutoCompact.event(input);
|
||||
await backgroundNotificationHook?.event(input);
|
||||
await sessionNotification?.(input);
|
||||
await todoContinuationEnforcer?.handler(input);
|
||||
await contextWindowMonitor?.event(input);
|
||||
await directoryAgentsInjector?.event(input);
|
||||
await directoryReadmeInjector?.event(input);
|
||||
await rulesInjector?.event(input);
|
||||
await thinkMode?.event(input);
|
||||
await anthropicAutoCompact?.event(input);
|
||||
await ultraworkMode?.event(input);
|
||||
|
||||
const { event } = input;
|
||||
const props = event.properties as Record<string, unknown> | undefined;
|
||||
@@ -313,7 +361,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const sessionID = props?.sessionID as string | undefined;
|
||||
const error = props?.error;
|
||||
|
||||
if (sessionRecovery.isRecoverableError(error)) {
|
||||
if (sessionRecovery?.isRecoverableError(error)) {
|
||||
const messageInfo = {
|
||||
id: props?.messageID as string | undefined,
|
||||
role: "assistant" as const,
|
||||
@@ -359,7 +407,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
"tool.execute.before": async (input, output) => {
|
||||
await claudeCodeHooks["tool.execute.before"](input, output);
|
||||
await commentChecker["tool.execute.before"](input, output);
|
||||
await commentChecker?.["tool.execute.before"](input, output);
|
||||
|
||||
if (input.sessionID === getMainSessionID()) {
|
||||
updateTerminalTitle({
|
||||
@@ -374,13 +422,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
"tool.execute.after": async (input, output) => {
|
||||
await claudeCodeHooks["tool.execute.after"](input, output);
|
||||
await grepOutputTruncator["tool.execute.after"](input, output);
|
||||
await contextWindowMonitor["tool.execute.after"](input, output);
|
||||
await commentChecker["tool.execute.after"](input, output);
|
||||
await directoryAgentsInjector["tool.execute.after"](input, output);
|
||||
await directoryReadmeInjector["tool.execute.after"](input, output);
|
||||
await rulesInjector["tool.execute.after"](input, output);
|
||||
await emptyTaskResponseDetector["tool.execute.after"](input, output);
|
||||
await grepOutputTruncator?.["tool.execute.after"](input, output);
|
||||
await contextWindowMonitor?.["tool.execute.after"](input, output);
|
||||
await commentChecker?.["tool.execute.after"](input, output);
|
||||
await directoryAgentsInjector?.["tool.execute.after"](input, output);
|
||||
await directoryReadmeInjector?.["tool.execute.after"](input, output);
|
||||
await rulesInjector?.["tool.execute.after"](input, output);
|
||||
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
||||
|
||||
if (input.sessionID === getMainSessionID()) {
|
||||
updateTerminalTitle({
|
||||
@@ -402,4 +450,5 @@ export type {
|
||||
AgentOverrideConfig,
|
||||
AgentOverrides,
|
||||
McpName,
|
||||
HookName,
|
||||
} from "./config";
|
||||
|
||||
@@ -62,21 +62,51 @@ function delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function truncateText(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text
|
||||
return text.slice(0, maxLength) + "..."
|
||||
}
|
||||
|
||||
function formatTaskStatus(task: BackgroundTask): string {
|
||||
const duration = formatDuration(task.startedAt, task.completedAt)
|
||||
const progress = task.progress
|
||||
? `\nTool calls: ${task.progress.toolCalls}\nLast tool: ${task.progress.lastTool ?? "N/A"}`
|
||||
: ""
|
||||
const promptPreview = truncateText(task.prompt, 500)
|
||||
|
||||
let progressSection = ""
|
||||
if (task.progress) {
|
||||
progressSection = `\nTool calls: ${task.progress.toolCalls}\nLast tool: ${task.progress.lastTool ?? "N/A"}`
|
||||
}
|
||||
|
||||
return `Task Status
|
||||
let lastMessageSection = ""
|
||||
if (task.progress?.lastMessage) {
|
||||
const truncated = truncateText(task.progress.lastMessage, 500)
|
||||
const messageTime = task.progress.lastMessageAt
|
||||
? task.progress.lastMessageAt.toISOString()
|
||||
: "N/A"
|
||||
lastMessageSection = `
|
||||
|
||||
Task ID: ${task.id}
|
||||
Description: ${task.description}
|
||||
Agent: ${task.agent}
|
||||
Status: ${task.status}
|
||||
Duration: ${duration}${progress}
|
||||
## Last Message (${messageTime})
|
||||
|
||||
Session ID: ${task.sessionID}`
|
||||
\`\`\`
|
||||
${truncated}
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
return `# Task Status
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Task ID | \`${task.id}\` |
|
||||
| Description | ${task.description} |
|
||||
| Agent | ${task.agent} |
|
||||
| Status | **${task.status}** |
|
||||
| Duration | ${duration} |
|
||||
| Session ID | \`${task.sessionID}\` |${progressSection}
|
||||
|
||||
## Original Prompt
|
||||
|
||||
\`\`\`
|
||||
${promptPreview}
|
||||
\`\`\`${lastMessageSection}`
|
||||
}
|
||||
|
||||
async function formatTaskResult(task: BackgroundTask, client: OpencodeClient): Promise<string> {
|
||||
|
||||
Reference in New Issue
Block a user