feat(hooks/atlas): end task timer when plan checkbox flips to checked via edit
This commit is contained in:
@@ -8,6 +8,7 @@ export function createAtlasHook(ctx: PluginInput, options?: AtlasHookOptions) {
|
|||||||
const sessions = new Map<string, SessionState>()
|
const sessions = new Map<string, SessionState>()
|
||||||
const pendingFilePaths = new Map<string, string>()
|
const pendingFilePaths = new Map<string, string>()
|
||||||
const pendingTaskRefs = new Map<string, PendingTaskRef>()
|
const pendingTaskRefs = new Map<string, PendingTaskRef>()
|
||||||
|
const pendingPlanSnapshots = new Map<string, string>()
|
||||||
const autoCommit = options?.autoCommit ?? true
|
const autoCommit = options?.autoCommit ?? true
|
||||||
|
|
||||||
function getState(sessionID: string): SessionState {
|
function getState(sessionID: string): SessionState {
|
||||||
@@ -25,12 +26,14 @@ export function createAtlasHook(ctx: PluginInput, options?: AtlasHookOptions) {
|
|||||||
ctx,
|
ctx,
|
||||||
pendingFilePaths,
|
pendingFilePaths,
|
||||||
pendingTaskRefs,
|
pendingTaskRefs,
|
||||||
|
pendingPlanSnapshots,
|
||||||
isCallerOrchestrator: options?.isCallerOrchestrator,
|
isCallerOrchestrator: options?.isCallerOrchestrator,
|
||||||
}),
|
}),
|
||||||
"tool.execute.after": createToolExecuteAfterHandler({
|
"tool.execute.after": createToolExecuteAfterHandler({
|
||||||
ctx,
|
ctx,
|
||||||
pendingFilePaths,
|
pendingFilePaths,
|
||||||
pendingTaskRefs,
|
pendingTaskRefs,
|
||||||
|
pendingPlanSnapshots,
|
||||||
autoCommit,
|
autoCommit,
|
||||||
getState,
|
getState,
|
||||||
isCallerOrchestrator: options?.isCallerOrchestrator,
|
isCallerOrchestrator: options?.isCallerOrchestrator,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ describe("createToolExecuteAfterHandler task timers", () => {
|
|||||||
session: {
|
session: {
|
||||||
get: async (input: SessionGetInput) => createSessionGetResult(parentSessionIDs?.[input.path.id]),
|
get: async (input: SessionGetInput) => createSessionGetResult(parentSessionIDs?.[input.path.id]),
|
||||||
},
|
},
|
||||||
} as unknown as PluginInput["client"]
|
} as PluginInput["client"]
|
||||||
|
|
||||||
if (parentSessionIDs) {
|
if (parentSessionIDs) {
|
||||||
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
|
||||||
@@ -88,6 +88,7 @@ describe("createToolExecuteAfterHandler task timers", () => {
|
|||||||
|
|
||||||
const pendingFilePaths = new Map<string, string>()
|
const pendingFilePaths = new Map<string, string>()
|
||||||
const pendingTaskRefs = new Map()
|
const pendingTaskRefs = new Map()
|
||||||
|
const pendingPlanSnapshots = new Map<string, string>()
|
||||||
const ctx = {
|
const ctx = {
|
||||||
client,
|
client,
|
||||||
project,
|
project,
|
||||||
@@ -98,11 +99,17 @@ describe("createToolExecuteAfterHandler task timers", () => {
|
|||||||
} satisfies PluginInput
|
} satisfies PluginInput
|
||||||
|
|
||||||
return {
|
return {
|
||||||
beforeHandler: createToolExecuteBeforeHandler({ ctx, pendingFilePaths, pendingTaskRefs }),
|
beforeHandler: createToolExecuteBeforeHandler({
|
||||||
|
ctx,
|
||||||
|
pendingFilePaths,
|
||||||
|
pendingTaskRefs,
|
||||||
|
pendingPlanSnapshots,
|
||||||
|
}),
|
||||||
afterHandler: createToolExecuteAfterHandler({
|
afterHandler: createToolExecuteAfterHandler({
|
||||||
ctx,
|
ctx,
|
||||||
pendingFilePaths,
|
pendingFilePaths,
|
||||||
pendingTaskRefs,
|
pendingTaskRefs,
|
||||||
|
pendingPlanSnapshots,
|
||||||
autoCommit: true,
|
autoCommit: true,
|
||||||
getState: () => ({ promptFailureCount: 0 }),
|
getState: () => ({ promptFailureCount: 0 }),
|
||||||
}),
|
}),
|
||||||
@@ -219,4 +226,70 @@ describe("createToolExecuteAfterHandler task timers", () => {
|
|||||||
expect(taskSession?.status).toBe("completed")
|
expect(taskSession?.status).toBe("completed")
|
||||||
expect(typeof taskSession?.elapsed_ms).toBe("number")
|
expect(typeof taskSession?.elapsed_ms).toBe("number")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("ends task timer when plan checkbox flips to checked via edit tool", async () => {
|
||||||
|
// given
|
||||||
|
const parentSessionID = "ses_parent_3"
|
||||||
|
const planPath = join(testDirectory, "task-timer-edit-plan.md")
|
||||||
|
writeFileSync(planPath, "# Plan\n\n## TODOs\n- [ ] 1. Implement auth flow\n", "utf-8")
|
||||||
|
writeBoulderState(testDirectory, {
|
||||||
|
schema_version: 2,
|
||||||
|
active_work_id: "work-1",
|
||||||
|
active_plan: planPath,
|
||||||
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
|
session_ids: [parentSessionID],
|
||||||
|
plan_name: "task-timer-edit-plan",
|
||||||
|
task_sessions: {
|
||||||
|
"todo:1": {
|
||||||
|
task_key: "todo:1",
|
||||||
|
task_label: "1",
|
||||||
|
task_title: "Implement auth flow",
|
||||||
|
session_id: "ses_child_3",
|
||||||
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
|
status: "running",
|
||||||
|
updated_at: "2026-01-02T10:00:00Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
works: {
|
||||||
|
"work-1": {
|
||||||
|
work_id: "work-1",
|
||||||
|
active_plan: planPath,
|
||||||
|
plan_name: "task-timer-edit-plan",
|
||||||
|
started_at: "2026-01-02T10:00:00Z",
|
||||||
|
session_ids: [parentSessionID],
|
||||||
|
status: "active",
|
||||||
|
task_sessions: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { beforeHandler, afterHandler } = createHandlers()
|
||||||
|
|
||||||
|
await beforeHandler(
|
||||||
|
{ tool: "edit", sessionID: parentSessionID, callID: "call-task-timer-edit-1" },
|
||||||
|
{ args: { filePath: planPath, oldString: "- [ ] 1. Implement auth flow", newString: "- [x] 1. Implement auth flow" } },
|
||||||
|
)
|
||||||
|
|
||||||
|
writeFileSync(planPath, "# Plan\n\n## TODOs\n- [x] 1. Implement auth flow\n", "utf-8")
|
||||||
|
|
||||||
|
// when
|
||||||
|
await afterHandler(
|
||||||
|
{ tool: "edit", sessionID: parentSessionID, callID: "call-task-timer-edit-1" },
|
||||||
|
{
|
||||||
|
title: "Edit",
|
||||||
|
output: "Updated file",
|
||||||
|
metadata: {
|
||||||
|
filePath: planPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// then
|
||||||
|
const taskSession = readBoulderState(testDirectory)?.works?.["work-1"]?.task_sessions?.["todo:1"]
|
||||||
|
expect(taskSession).toBeDefined()
|
||||||
|
expect(taskSession?.ended_at).toBeString()
|
||||||
|
expect(taskSession?.status).toBe("completed")
|
||||||
|
expect(typeof taskSession?.elapsed_ms).toBe("number")
|
||||||
|
expect((taskSession?.elapsed_ms ?? 0) > 0).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
upsertTaskSessionState,
|
upsertTaskSessionState,
|
||||||
} from "../../features/boulder-state"
|
} from "../../features/boulder-state"
|
||||||
import { existsSync, readFileSync } from "node:fs"
|
import { existsSync, readFileSync } from "node:fs"
|
||||||
|
import { resolve } from "node:path"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import { isCallerOrchestrator } from "../../shared/session-utils"
|
import { isCallerOrchestrator } from "../../shared/session-utils"
|
||||||
import { syncBackgroundLaunchSessionTracking } from "./background-launch-session-tracking"
|
import { syncBackgroundLaunchSessionTracking } from "./background-launch-session-tracking"
|
||||||
@@ -59,15 +60,77 @@ function isTrackedTaskChecked(planPath: string, taskKey: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TODO_HEADING_PATTERN = /^##\s+TODOs\b/i
|
||||||
|
const FINAL_VERIFICATION_HEADING_PATTERN = /^##\s+Final Verification Wave\b/i
|
||||||
|
const SECOND_LEVEL_HEADING_PATTERN = /^##\s+/
|
||||||
|
const CHECKED_CHECKBOX_PATTERN = /^(\s*)[-*]\s*\[[xX]\]\s*(.+)$/
|
||||||
|
const TODO_TASK_PATTERN = /^(\d+)\.\s+(.+)$/
|
||||||
|
const FINAL_WAVE_TASK_PATTERN = /^(F\d+)\.\s+(.+)$/i
|
||||||
|
|
||||||
|
function parseCheckedTopLevelTaskKeys(planContent: string): Set<string> {
|
||||||
|
const checkedKeys = new Set<string>()
|
||||||
|
const lines = planContent.split(/\r?\n/)
|
||||||
|
let section: "todo" | "final-wave" | "other" = "other"
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (SECOND_LEVEL_HEADING_PATTERN.test(line)) {
|
||||||
|
section = TODO_HEADING_PATTERN.test(line)
|
||||||
|
? "todo"
|
||||||
|
: FINAL_VERIFICATION_HEADING_PATTERN.test(line)
|
||||||
|
? "final-wave"
|
||||||
|
: "other"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section !== "todo" && section !== "final-wave") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkedMatch = line.match(CHECKED_CHECKBOX_PATTERN)
|
||||||
|
if (!checkedMatch || checkedMatch[1].length > 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskBody = checkedMatch[2].trim()
|
||||||
|
if (section === "todo") {
|
||||||
|
const taskMatch = taskBody.match(TODO_TASK_PATTERN)
|
||||||
|
if (taskMatch?.[1]) {
|
||||||
|
checkedKeys.add(`todo:${taskMatch[1]}`)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskMatch = taskBody.match(FINAL_WAVE_TASK_PATTERN)
|
||||||
|
if (taskMatch?.[1]) {
|
||||||
|
checkedKeys.add(`final-wave:${taskMatch[1].toLowerCase()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkedKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCheckedTaskKeysFromPlan(planPath: string): Set<string> {
|
||||||
|
if (!existsSync(planPath)) {
|
||||||
|
return new Set<string>()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return parseCheckedTopLevelTaskKeys(readFileSync(planPath, "utf-8"))
|
||||||
|
} catch {
|
||||||
|
return new Set<string>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createToolExecuteAfterHandler(input: {
|
export function createToolExecuteAfterHandler(input: {
|
||||||
ctx: PluginInput
|
ctx: PluginInput
|
||||||
pendingFilePaths: Map<string, string>
|
pendingFilePaths: Map<string, string>
|
||||||
pendingTaskRefs: Map<string, PendingTaskRef>
|
pendingTaskRefs: Map<string, PendingTaskRef>
|
||||||
|
pendingPlanSnapshots?: Map<string, string>
|
||||||
autoCommit: boolean
|
autoCommit: boolean
|
||||||
getState: (sessionID: string) => SessionState
|
getState: (sessionID: string) => SessionState
|
||||||
isCallerOrchestrator?: (sessionID: string | undefined) => Promise<boolean>
|
isCallerOrchestrator?: (sessionID: string | undefined) => Promise<boolean>
|
||||||
}): (toolInput: ToolExecuteAfterInput, toolOutput: ToolExecuteAfterOutput | undefined) => Promise<void> {
|
}): (toolInput: ToolExecuteAfterInput, toolOutput: ToolExecuteAfterOutput | undefined) => Promise<void> {
|
||||||
const { ctx, pendingFilePaths, pendingTaskRefs, autoCommit, getState } = input
|
const { ctx, pendingFilePaths, pendingTaskRefs, pendingPlanSnapshots, autoCommit, getState } = input
|
||||||
const resolveIsCallerOrchestrator = input.isCallerOrchestrator ?? ((sessionID) => isCallerOrchestrator(sessionID, ctx.client))
|
const resolveIsCallerOrchestrator = input.isCallerOrchestrator ?? ((sessionID) => isCallerOrchestrator(sessionID, ctx.client))
|
||||||
return async (toolInput, toolOutput): Promise<void> => {
|
return async (toolInput, toolOutput): Promise<void> => {
|
||||||
// Guard against undefined output (e.g., from /review command - see issue #1035)
|
// Guard against undefined output (e.g., from /review command - see issue #1035)
|
||||||
@@ -81,12 +144,33 @@ export function createToolExecuteAfterHandler(input: {
|
|||||||
|
|
||||||
if (isWriteOrEditToolName(toolInput.tool)) {
|
if (isWriteOrEditToolName(toolInput.tool)) {
|
||||||
let filePath = toolInput.callID ? pendingFilePaths.get(toolInput.callID) : undefined
|
let filePath = toolInput.callID ? pendingFilePaths.get(toolInput.callID) : undefined
|
||||||
|
const planSnapshot = toolInput.callID && pendingPlanSnapshots
|
||||||
|
? pendingPlanSnapshots.get(toolInput.callID)
|
||||||
|
: undefined
|
||||||
if (toolInput.callID) {
|
if (toolInput.callID) {
|
||||||
pendingFilePaths.delete(toolInput.callID)
|
pendingFilePaths.delete(toolInput.callID)
|
||||||
|
pendingPlanSnapshots?.delete(toolInput.callID)
|
||||||
}
|
}
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
filePath = toolOutput.metadata?.filePath as string | undefined
|
filePath = toolOutput.metadata?.filePath as string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filePath && toolInput.sessionID) {
|
||||||
|
const sessionWork = getWorkForSession(ctx.directory, toolInput.sessionID)
|
||||||
|
if (sessionWork) {
|
||||||
|
const planPath = resolveBoulderPlanPathForWork(ctx.directory, sessionWork)
|
||||||
|
if (resolve(filePath) === resolve(planPath) && planSnapshot !== undefined) {
|
||||||
|
const beforeCheckedKeys = parseCheckedTopLevelTaskKeys(planSnapshot)
|
||||||
|
const afterCheckedKeys = readCheckedTaskKeysFromPlan(planPath)
|
||||||
|
for (const taskKey of afterCheckedKeys) {
|
||||||
|
if (!beforeCheckedKeys.has(taskKey)) {
|
||||||
|
endTaskTimer(ctx.directory, sessionWork.work_id, taskKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (filePath && !isSisyphusPath(filePath)) {
|
if (filePath && !isSisyphusPath(filePath)) {
|
||||||
toolOutput.output = (toolOutput.output || "") + DIRECT_WORK_REMINDER
|
toolOutput.output = (toolOutput.output || "") + DIRECT_WORK_REMINDER
|
||||||
log(`[${HOOK_NAME}] Direct work reminder appended`, {
|
log(`[${HOOK_NAME}] Direct work reminder appended`, {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { log } from "../../shared/logger"
|
|||||||
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
||||||
import { isCallerOrchestrator } from "../../shared/session-utils"
|
import { isCallerOrchestrator } from "../../shared/session-utils"
|
||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { readBoulderState, readCurrentTopLevelTask, resolveBoulderPlanPath } from "../../features/boulder-state"
|
import { existsSync, readFileSync } from "node:fs"
|
||||||
|
import { resolve } from "node:path"
|
||||||
|
import { getWorkForSession, readBoulderState, readCurrentTopLevelTask, resolveBoulderPlanPath, resolveBoulderPlanPathForWork } from "../../features/boulder-state"
|
||||||
import { HOOK_NAME } from "./hook-name"
|
import { HOOK_NAME } from "./hook-name"
|
||||||
import { ORCHESTRATOR_DELEGATION_REQUIRED, SINGLE_TASK_DIRECTIVE } from "./system-reminder-templates"
|
import { ORCHESTRATOR_DELEGATION_REQUIRED, SINGLE_TASK_DIRECTIVE } from "./system-reminder-templates"
|
||||||
import { isSisyphusPath } from "./sisyphus-path"
|
import { isSisyphusPath } from "./sisyphus-path"
|
||||||
@@ -13,12 +15,13 @@ export function createToolExecuteBeforeHandler(input: {
|
|||||||
ctx: PluginInput
|
ctx: PluginInput
|
||||||
pendingFilePaths: Map<string, string>
|
pendingFilePaths: Map<string, string>
|
||||||
pendingTaskRefs: Map<string, PendingTaskRef>
|
pendingTaskRefs: Map<string, PendingTaskRef>
|
||||||
|
pendingPlanSnapshots?: Map<string, string>
|
||||||
isCallerOrchestrator?: (sessionID: string | undefined) => Promise<boolean>
|
isCallerOrchestrator?: (sessionID: string | undefined) => Promise<boolean>
|
||||||
}): (
|
}): (
|
||||||
toolInput: { tool: string; sessionID?: string; callID?: string },
|
toolInput: { tool: string; sessionID?: string; callID?: string },
|
||||||
toolOutput: { args: Record<string, unknown>; message?: string }
|
toolOutput: { args: Record<string, unknown>; message?: string }
|
||||||
) => Promise<void> {
|
) => Promise<void> {
|
||||||
const { ctx, pendingFilePaths, pendingTaskRefs } = input
|
const { ctx, pendingFilePaths, pendingTaskRefs, pendingPlanSnapshots } = input
|
||||||
const resolveIsCallerOrchestrator = input.isCallerOrchestrator ?? ((sessionID) => isCallerOrchestrator(sessionID, ctx.client))
|
const resolveIsCallerOrchestrator = input.isCallerOrchestrator ?? ((sessionID) => isCallerOrchestrator(sessionID, ctx.client))
|
||||||
|
|
||||||
function trackTask(callID: string, task: TrackedTopLevelTaskRef): void {
|
function trackTask(callID: string, task: TrackedTopLevelTaskRef): void {
|
||||||
@@ -38,6 +41,27 @@ export function createToolExecuteBeforeHandler(input: {
|
|||||||
// Store filePath for use in tool.execute.after
|
// Store filePath for use in tool.execute.after
|
||||||
if (toolInput.callID) {
|
if (toolInput.callID) {
|
||||||
pendingFilePaths.set(toolInput.callID, filePath)
|
pendingFilePaths.set(toolInput.callID, filePath)
|
||||||
|
|
||||||
|
const sessionID = toolInput.sessionID
|
||||||
|
const sessionWork = sessionID
|
||||||
|
? getWorkForSession(ctx.directory, sessionID)
|
||||||
|
: null
|
||||||
|
const state = sessionWork ? null : readBoulderState(ctx.directory)
|
||||||
|
const planPath = sessionWork
|
||||||
|
? resolveBoulderPlanPathForWork(ctx.directory, sessionWork)
|
||||||
|
: state
|
||||||
|
? resolveBoulderPlanPath(ctx.directory, state)
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (planPath && resolve(filePath) === resolve(planPath) && pendingPlanSnapshots) {
|
||||||
|
try {
|
||||||
|
if (existsSync(planPath)) {
|
||||||
|
pendingPlanSnapshots.set(toolInput.callID, readFileSync(planPath, "utf-8"))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
pendingPlanSnapshots.delete(toolInput.callID)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const warning = ORCHESTRATOR_DELEGATION_REQUIRED.replace("$FILE_PATH", filePath)
|
const warning = ORCHESTRATOR_DELEGATION_REQUIRED.replace("$FILE_PATH", filePath)
|
||||||
toolOutput.message = (toolOutput.message || "") + warning
|
toolOutput.message = (toolOutput.message || "") + warning
|
||||||
@@ -65,28 +89,28 @@ export function createToolExecuteBeforeHandler(input: {
|
|||||||
? readCurrentTopLevelTask(resolveBoulderPlanPath(ctx.directory, boulderState))
|
? readCurrentTopLevelTask(resolveBoulderPlanPath(ctx.directory, boulderState))
|
||||||
: null
|
: null
|
||||||
if (currentTask) {
|
if (currentTask) {
|
||||||
const task = {
|
const trackedTask = {
|
||||||
key: currentTask.key,
|
key: currentTask.key,
|
||||||
label: currentTask.label,
|
label: currentTask.label,
|
||||||
title: currentTask.title,
|
title: currentTask.title,
|
||||||
}
|
}
|
||||||
const hasExistingClaim = [...pendingTaskRefs.values()].some((pendingTaskRef) => (
|
const hasExistingClaim = [...pendingTaskRefs.values()].some((pendingTaskRef) => (
|
||||||
pendingTaskRef.kind === "track" && pendingTaskRef.task.key === task.key
|
pendingTaskRef.kind === "track" && pendingTaskRef.task.key === trackedTask.key
|
||||||
))
|
))
|
||||||
|
|
||||||
if (hasExistingClaim) {
|
if (hasExistingClaim) {
|
||||||
pendingTaskRefs.set(toolInput.callID, {
|
pendingTaskRefs.set(toolInput.callID, {
|
||||||
kind: "skip",
|
kind: "skip",
|
||||||
reason: "ambiguous_task_key",
|
reason: "ambiguous_task_key",
|
||||||
task,
|
task: trackedTask,
|
||||||
})
|
})
|
||||||
log(`[${HOOK_NAME}] Skipping task session persistence for ambiguous task key`, {
|
log(`[${HOOK_NAME}] Skipping task session persistence for ambiguous task key`, {
|
||||||
sessionID: toolInput.sessionID,
|
sessionID: toolInput.sessionID,
|
||||||
callID: toolInput.callID,
|
callID: toolInput.callID,
|
||||||
taskKey: task.key,
|
taskKey: trackedTask.key,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
trackTask(toolInput.callID, task)
|
trackTask(toolInput.callID, trackedTask)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const WRITE_EDIT_TOOLS = ["Write", "Edit", "write", "edit"]
|
const WRITE_EDIT_TOOLS = ["Write", "Edit", "write", "edit", "hashline_edit"]
|
||||||
|
|
||||||
export function isWriteOrEditToolName(toolName: string): boolean {
|
export function isWriteOrEditToolName(toolName: string): boolean {
|
||||||
return WRITE_EDIT_TOOLS.includes(toolName)
|
return WRITE_EDIT_TOOLS.includes(toolName)
|
||||||
|
|||||||
Reference in New Issue
Block a user