Merge remote-tracking branch 'origin/main' into fix/agent-manager-attribution

This commit is contained in:
marius-kilocode
2026-05-15 12:54:00 +02:00
46 changed files with 691 additions and 104 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep continued Agent Manager sessions in the selected worktree instead of moving them to Local.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Support speech-to-text voice input in Agent Manager inline review comments.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Send recorded voice prompts directly when the prompt send button is pressed.
+11 -1
View File
@@ -41,11 +41,21 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
# kilocode_change start
- name: Setup Node
uses: actions/setup-node@v6 # kilocode_change
id: setup-node
continue-on-error: ${{ runner.os == 'Windows' }}
uses: actions/setup-node@v6
with:
node-version: "24"
- name: Retry Setup Node on Windows
if: runner.os == 'Windows' && steps.setup-node.outcome == 'failure'
uses: actions/setup-node@v6
with:
node-version: "24"
# kilocode_change end
- name: Setup Bun
uses: ./.github/actions/setup-bun
+23 -14
View File
@@ -137,6 +137,7 @@ import type { KiloProviderOptions } from "./kilo-provider/options"
import { fetchKiloEmbeddingModelCatalog } from "@kilocode/kilo-gateway"
type MessageLoadMode = "replace" | "prepend" | "focus" | "reconcile"
type ContextMessage = { contextDirectory?: unknown }
// Helper to map agent data to the subset of fields sent to the webview
const mapAgent = (a: Agent) => ({
name: a.name,
@@ -647,7 +648,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.readyResolvers.splice(0).forEach((r) => r())
break
case "sendMessage": {
const files = parseMessageFiles(message.files)
const msg = message as typeof message & ContextMessage
await this.handleSendMessage(
message.text,
typeof message.messageID === "string" ? message.messageID : undefined,
@@ -657,13 +658,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
message.modelID,
message.agent,
message.variant,
files,
parseMessageFiles(message.files),
typeof message.agentManagerContext === "string" ? message.agentManagerContext : undefined,
typeof msg.contextDirectory === "string" ? msg.contextDirectory : undefined,
)
break
}
case "sendCommand": {
const files = parseMessageFiles(message.files)
const msg = message as typeof message & ContextMessage
await this.handleSendCommand(
message.command,
message.arguments,
@@ -674,8 +676,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
message.modelID,
message.agent,
message.variant,
files,
parseMessageFiles(message.files),
typeof message.agentManagerContext === "string" ? message.agentManagerContext : undefined,
typeof msg.contextDirectory === "string" ? msg.contextDirectory : undefined,
)
break
}
@@ -1081,13 +1084,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
break
}
case "fetchMarketplaceData": {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const mp = this.getMarketplace()
// Fetch skills from CLI backend (authoritative source) so the
// marketplace doesn't need to duplicate the CLI's skill scanning.
const skills = await this.fetchCliSkills()
const data = await mp.fetchData(workspace, skills)
this.postMessage({ type: "marketplaceData", ...data })
await this.handleFetchMarketplaceData()
break
}
case "filterMarketplaceItems": {
@@ -1137,6 +1134,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.diffVirtualProvider.open(d)
}
private async handleFetchMarketplaceData(): Promise<void> {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const mp = this.getMarketplace()
const skills = await this.fetchCliSkills()
const data = await mp.fetchData(workspace, skills)
this.postMessage({ type: "marketplaceData", ...data })
}
/**
* Initialize connection to the CLI backend server.
* Subscribes to the shared KiloConnectionService.
@@ -1539,6 +1544,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
client.session.list({ directory: dir, roots: true }, { throwOnError: true }).then(({ data }) => data)
: null,
sessionDirectories: this.sessionDirectories,
worktreeDirectories: this.opts.worktreeDirectories,
workspaceDirectory: this.getWorkspaceDirectory(),
postMessage: (msg: unknown) => this.postMessage(msg),
}
@@ -2442,7 +2448,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
details: getConfigErrorDetails(error),
})
}
private async resolveSession(sessionID?: string, draftID?: string, context?: string) {
private async resolveSession(sessionID?: string, draftID?: string, context?: string, contextDirectory?: string) {
if (!this.client) return undefined
const dir = resolveNewSessionDirectory({
@@ -2450,6 +2456,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
currentSessionID: this.currentSession?.id,
contextSessionID: this.contextSessionID,
agentManagerContext: context,
contextDirectory,
sessionDirectories: this.sessionDirectories,
workspaceDirectory: this.getRootDirectory(),
})
@@ -2562,6 +2569,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
variant?: string,
files?: MessageFile[],
context?: string,
contextDirectory?: string,
): Promise<void> {
if (!this.client) {
this.postMessage({
@@ -2578,7 +2586,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
let resolved: { sid: string; dir: string } | undefined
try {
resolved = await this.resolveSession(sessionID, draftID, context)
resolved = await this.resolveSession(sessionID, draftID, context, contextDirectory)
const parts: Array<TextPartInput | FilePartInput> = []
if (files) {
@@ -2639,6 +2647,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
variant?: string,
files?: MessageFile[],
context?: string,
contextDirectory?: string,
): Promise<void> {
if (!this.client) {
this.postMessage({
@@ -2655,7 +2664,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
let resolved: { sid: string; dir: string } | undefined
try {
resolved = await this.resolveSession(sessionID, draftID, context)
resolved = await this.resolveSession(sessionID, draftID, context, contextDirectory)
if (messageID) {
this.connectionService.recordMessageSessionId(messageID, resolved!.sid)
@@ -181,6 +181,7 @@ export class AgentManagerProvider implements Disposable {
this.attachPanel(
this.host.openPanel({
onBeforeMessage: (msg) => this.onMessage(msg),
worktreeDirectories: () => this.getWorktreeDirectories(),
}),
)
}
@@ -378,7 +379,7 @@ export class AgentManagerProvider implements Disposable {
if (m.type === "agentManager.deleteWorktree") return this.onDeleteWorktree(m.worktreeId)
if (m.type === "agentManager.removeStaleWorktree") return this.onRemoveStaleWorktree(m.worktreeId)
if (m.type === "agentManager.promoteSession") return this.onPromoteSession(m.sessionId)
if (m.type === "agentManager.addSessionToWorktree") return this.onAddSessionToWorktree(m.worktreeId)
if (m.type === "agentManager.addSessionToWorktree") return this.onAddSessionToWorktree(m.worktreeId, m.sessionId)
if (m.type === "agentManager.forkSession") return this.onForkSession(m.sessionId, m.worktreeId, m.messageId)
if (m.type === "agentManager.closeSession") return this.onCloseSession(m.sessionId)
}
@@ -418,6 +419,15 @@ export class AgentManagerProvider implements Disposable {
return null
}
if ((m.type === "sendMessage" || m.type === "sendCommand") && !m.sessionID) {
const ctx = typeof m.agentManagerContext === "string" ? m.agentManagerContext : undefined
const worktree = ctx && ctx !== "local" ? this.getStateManager()?.getWorktree(ctx) : undefined
if (worktree) {
if (m.draftID) this.activeSessionId = m.draftID
return { ...msg, contextDirectory: worktree.path }
}
}
if ((m.type === "sendMessage" || m.type === "sendCommand") && m.draftID && !m.sessionID) {
this.activeSessionId = m.draftID
return msg
@@ -1049,7 +1059,7 @@ export class AgentManagerProvider implements Disposable {
}
/** Add a new session to an existing worktree. */
private async onAddSessionToWorktree(worktreeId: string): Promise<null> {
private async onAddSessionToWorktree(worktreeId: string, sessionId?: string): Promise<null> {
let client: KiloClient
try {
client = this.connectionService.getClient()
@@ -1068,6 +1078,26 @@ export class AgentManagerProvider implements Disposable {
return null
}
if (sessionId) {
if (state.getSession(sessionId)) state.moveSession(sessionId, worktreeId)
else state.addSession(sessionId, worktreeId)
this.registerWorktreeSession(sessionId, worktree.path)
this.pushState()
this.postToWebview({
type: "agentManager.sessionAdded",
sessionId,
worktreeId,
})
this.host.capture("Agent Manager Session Started", {
source: PLATFORM,
sessionId,
worktreeId,
existing: true,
})
this.log(`Added existing session ${sessionId} to worktree ${worktreeId}`)
return null
}
let session: Session
try {
const { data } = await client.session.create(
@@ -1648,6 +1678,14 @@ export class AgentManagerProvider implements Disposable {
return this.panel?.sessions.getSessionDirectories() ?? new Map()
}
public getWorktreeDirectories(): string[] {
return (
this.getStateManager()
?.getWorktrees()
.map((wt) => wt.path) ?? []
)
}
/**
* Continue a sidebar session in a new worktree.
* Captures git state, creates worktree, applies state, forks session.
@@ -101,6 +101,7 @@ function createHarness() {
prBridge: { handleMessage: ReturnType<typeof vi.fn> }
activeSessionId: string | undefined
stateReady: Promise<void> | undefined
contextTarget: ReturnType<typeof vi.fn>
createWorktreeOnDisk: ReturnType<typeof vi.fn>
runSetupScriptForWorktree: ReturnType<typeof vi.fn>
createSessionInWorktree: ReturnType<typeof vi.fn>
@@ -121,6 +122,7 @@ function createHarness() {
manager.prBridge = { handleMessage: vi.fn().mockReturnValue(false) }
manager.activeSessionId = undefined
manager.stateReady = Promise.resolve()
manager.contextTarget = vi.fn()
manager.createWorktreeOnDisk = vi.fn()
manager.runSetupScriptForWorktree = vi.fn().mockResolvedValue(undefined)
manager.createSessionInWorktree = vi.fn()
@@ -202,4 +204,28 @@ describe("AgentManagerProvider worktree creation", () => {
expect(result).toEqual({ type: "requestFileSearch", query: "src", requestId: "r1", sessionID: "session-wt" })
})
it("resolves new sends to the selected worktree directory", async () => {
const manager = createHarness()
const state = {
getWorktree: vi.fn().mockReturnValue({ id: "wt-1", path: "/repo/.kilo/worktrees/wt-1" }),
}
manager.getStateManager.mockReturnValue(state)
manager.contextTarget.mockResolvedValue(undefined)
const result = await manager.onMessage({
type: "sendMessage",
text: "continue",
agentManagerContext: "wt-1",
draftID: "draft-1",
})
expect(result).toEqual({
type: "sendMessage",
text: "continue",
agentManagerContext: "wt-1",
draftID: "draft-1",
contextDirectory: "/repo/.kilo/worktrees/wt-1",
})
})
})
@@ -93,6 +93,7 @@ export interface Host {
*/
openPanel(opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
}): PanelContext
/** Get the workspace/project root path. */
@@ -348,6 +348,7 @@ interface OpenLocallyIn {
interface AddSessionToWorktreeIn {
type: "agentManager.addSessionToWorktree"
worktreeId: string
sessionId?: string
}
interface CloseSessionIn {
@@ -606,6 +607,7 @@ interface SendMessageIn {
variant?: string
files?: Array<{ mime: string; url: string; filename?: string; source?: FileSourceIn }>
agentManagerContext?: string
contextDirectory?: string
}
interface SendCommandIn {
@@ -621,6 +623,7 @@ interface SendCommandIn {
variant?: string
files?: Array<{ mime: string; url: string; filename?: string; source?: FileSourceIn }>
agentManagerContext?: string
contextDirectory?: string
}
interface RequestTerminalContextIn {
@@ -36,6 +36,7 @@ export class VscodeHost implements Host {
openPanel(opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
}): PanelContext {
const panel = vscode.window.createWebviewPanel(
"kilo-code.new.AgentManagerPanel",
@@ -55,6 +56,7 @@ export class VscodeHost implements Host {
panel: vscode.WebviewPanel,
opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
},
): PanelContext {
return this.wirePanel(panel, opts)
@@ -64,6 +66,7 @@ export class VscodeHost implements Host {
panel: vscode.WebviewPanel,
opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
},
): PanelContext {
panel.webview.options = {
@@ -88,6 +91,7 @@ export class VscodeHost implements Host {
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, {
platform: PLATFORM,
slimEditMetadata: true,
worktreeDirectories: () => opts.worktreeDirectories?.() ?? [],
})
if (this.diffVirtual) {
provider.setDiffVirtualProvider(this.diffVirtual)
+1
View File
@@ -171,6 +171,7 @@ export function activate(context: vscode.ExtensionContext) {
deserializeWebviewPanel(panel: vscode.WebviewPanel) {
const ctx = agentManagerHost.wrapExistingPanel(panel, {
onBeforeMessage: (msg) => agentManagerProvider.handleMessage(msg),
worktreeDirectories: () => agentManagerProvider.getWorktreeDirectories(),
})
agentManagerProvider.deserializePanel(ctx)
return Promise.resolve()
@@ -222,6 +222,7 @@ export interface SessionRefreshContext {
connectionState: "connecting" | "connected" | "disconnected" | "error"
listSessions: ((dir: string) => Promise<Session[]>) | null
sessionDirectories: Map<string, string>
worktreeDirectories?: () => string[]
workspaceDirectory: string
postMessage(message: unknown): void
}
@@ -245,7 +246,7 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise<string |
const sessions = await list(ctx.workspaceDirectory)
const projectID = sessions[0]?.projectID
const worktreeDirs = new Set(ctx.sessionDirectories.values())
const worktreeDirs = new Set([...(ctx.worktreeDirectories?.() ?? []), ...ctx.sessionDirectories.values()])
const failed = new Set<string>()
const extra = await Promise.all(
[...worktreeDirs].map((dir) =>
@@ -339,6 +340,7 @@ export function resolveNewSessionDirectory(input: {
currentSessionID?: string
contextSessionID?: string
agentManagerContext?: string
contextDirectory?: string
sessionDirectories: Map<string, string>
workspaceDirectory: string
}) {
@@ -350,6 +352,8 @@ export function resolveNewSessionDirectory(input: {
})
}
if (input.contextDirectory) return input.contextDirectory
return resolveContextDirectory({
currentSessionID: input.currentSessionID,
contextSessionID: input.contextSessionID,
@@ -3,4 +3,5 @@ export type KiloProviderOptions = {
platform?: string
slimEditMetadata?: boolean
tabTitle?: (title: string) => void
worktreeDirectories?: () => string[]
}
@@ -30,6 +30,7 @@ const TSX_FILES = [
path.join(ROOT, "webview-ui/agent-manager/DiffEndMarker.tsx"),
path.join(ROOT, "webview-ui/agent-manager/FileTree.tsx"),
path.join(ROOT, "webview-ui/agent-manager/review-annotations.ts"),
path.join(ROOT, "webview-ui/agent-manager/review-annotation-speech.tsx"),
path.join(ROOT, "webview-ui/agent-manager/MultiModelSelector.tsx"),
path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"),
path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"),
@@ -93,6 +93,17 @@ describe("resolveNewSessionDirectory", () => {
expect(dir).toBe("/repo/.kilo/worktrees/feature")
})
it("uses explicit Agent Manager worktree context after the active session was cleared", () => {
const dir = resolveNewSessionDirectory({
agentManagerContext: "wt_feature",
contextDirectory: "/repo/.kilo/worktrees/feature",
sessionDirectories: new Map(),
workspaceDirectory: "/repo",
})
expect(dir).toBe("/repo/.kilo/worktrees/feature")
})
it("creates local Agent Manager sessions in the workspace root after a worktree was selected", () => {
const dir = resolveNewSessionDirectory({
contextSessionID: "ses_worktree",
@@ -8,6 +8,11 @@ import {
type ReviewComment,
} from "../../webview-ui/agent-manager/review-comments"
import { markdownCommentBlocks } from "../../webview-ui/agent-manager/markdown-comment-ranges"
import {
reviewAnnotationSpeechKey,
reviewDraftSpeechKey,
reviewEditSpeechKey,
} from "../../webview-ui/agent-manager/review-annotations"
import type { WorktreeFileDiff } from "../../webview-ui/src/types/messages"
function diff(file: string, before: string, after: string): WorktreeFileDiff {
@@ -236,6 +241,53 @@ describe("markdownCommentBlocks", () => {
})
})
// ── review annotation speech keys ───────────────────────────────────────────
describe("review annotation speech keys", () => {
it("keeps draft keys scoped to the selected review location", () => {
expect(reviewDraftSpeechKey({ file: "src/a.ts", side: "additions", line: 4 })).toBe("draft:src/a.ts:additions:4:4")
expect(reviewDraftSpeechKey({ file: "src/a.ts", side: "deletions", line: 4, endLine: 8 })).toBe(
"draft:src/a.ts:deletions:4:8",
)
})
it("keeps edit keys scoped to the review comment id", () => {
expect(reviewEditSpeechKey("c-123")).toBe("edit:c-123")
})
it("only exposes annotation speech keys for active draft and edit composers", () => {
const current = comment({ file: "src/a.ts", line: 7 })
expect(
reviewAnnotationSpeechKey({
type: "draft",
comment: null,
file: "src/a.ts",
side: "additions",
line: 7,
}),
).toBe("draft:src/a.ts:additions:7:7")
expect(
reviewAnnotationSpeechKey({
type: "comment",
comment: current,
file: current.file,
side: current.side,
line: current.line,
editing: true,
}),
).toBe(`edit:${current.id}`)
expect(
reviewAnnotationSpeechKey({
type: "comment",
comment: current,
file: current.file,
side: current.side,
line: current.line,
}),
).toBeUndefined()
})
})
// ── getDirectory / getFilename ──────────────────────────────────────────────
describe("getDirectory", () => {
@@ -0,0 +1,84 @@
import { describe, expect, it } from "bun:test"
import { createRoot } from "solid-js"
import { useSpeechToText } from "../../webview-ui/src/components/speech-to-text/useSpeechToText"
import type { ExtensionMessage, WebviewMessage } from "../../webview-ui/src/types/messages"
function setup() {
const sent: WebviewMessage[] = []
let handler: ((message: ExtensionMessage) => void) | undefined
const root = createRoot((dispose) => ({
dispose,
speech: useSpeechToText(
{
postMessage: (message) => sent.push(message),
onMessage: (next) => {
handler = next
return () => {
handler = undefined
}
},
},
{ profileData: () => ({}), goToLogin: () => {} },
{ t: (key) => key },
),
}))
const fire = (message: ExtensionMessage) => handler?.(message)
return { ...root, fire, sent }
}
describe("useSpeechToText", () => {
it("runs the stop completion after inserting a transcript", () => {
const ctx = setup()
const text: string[] = []
let done = 0
ctx.speech.start({ model: "scribe", insert: (value) => text.push(value) })
const start = ctx.sent[0]
if (start?.type !== "speechToTextStart") throw new Error("speech start message missing")
ctx.speech.stop({ done: () => done++ })
ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Recorded prompt" })
expect(text).toEqual(["Recorded prompt"])
expect(done).toBe(1)
expect(ctx.speech.state()).toBe("idle")
ctx.dispose()
})
it("drops the stop completion when transcription is cancelled", () => {
const ctx = setup()
let done = 0
ctx.speech.start({ model: "scribe", insert: () => {} })
const start = ctx.sent[0]
if (start?.type !== "speechToTextStart") throw new Error("speech start message missing")
ctx.speech.stop({ done: () => done++ })
ctx.speech.cancel()
ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Ignored prompt" })
expect(done).toBe(0)
expect(ctx.speech.state()).toBe("idle")
ctx.dispose()
})
it("drops the stop completion when the send context changes", () => {
const ctx = setup()
const text: string[] = []
let done = 0
ctx.speech.start({ model: "scribe", insert: (value) => text.push(value) })
const start = ctx.sent[0]
if (start?.type !== "speechToTextStart") throw new Error("speech start message missing")
ctx.speech.stop({ done: () => done++, ready: () => false })
ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Keep as draft" })
expect(text).toEqual(["Keep as draft"])
expect(done).toBe(0)
expect(ctx.speech.state()).toBe("idle")
ctx.dispose()
})
})
@@ -130,6 +130,7 @@ import { createMarkdownRender } from "./review-preferences"
import { createSidebarCollapse } from "./sidebar-collapse"
import { SidebarToggleButton } from "./SidebarToggleButton"
import { setTabWidths } from "./tab-widths"
import { buildShortcutCategories } from "./shortcuts"
import "./agent-manager.css"
import "./agent-manager-review.css"
const REVIEW_TAB_ID = "review"
@@ -185,76 +186,6 @@ const defaultBindings: Record<string, string> = {
),
}
/** Shortcut category definition for the keyboard shortcuts dialog */
interface ShortcutEntry {
label: string
binding: string
}
interface ShortcutCategory {
title: string
shortcuts: ShortcutEntry[]
}
/** Build the categorized list of keyboard shortcuts from the current bindings */
function buildShortcutCategories(
bindings: Record<string, string>,
t: (key: string, params?: Record<string, string | number>) => string,
): ShortcutCategory[] {
return [
{
title: t("agentManager.shortcuts.category.quickSwitch"),
shortcuts: [
{
label: t("agentManager.shortcuts.jumpToItem"),
binding: (() => {
const first = bindings.jumpTo1 ?? ""
const prefix = first.replace(/\d+$/, "")
return prefix ? `${prefix}1-9` : ""
})(),
},
],
},
{
title: t("agentManager.shortcuts.category.sidebar"),
shortcuts: [
{ label: t("agentManager.shortcuts.previousItem"), binding: bindings.previousSession ?? "" },
{ label: t("agentManager.shortcuts.nextItem"), binding: bindings.nextSession ?? "" },
{ label: t("agentManager.shortcuts.newWorktree"), binding: bindings.newWorktree ?? "" },
{ label: t("agentManager.shortcuts.advancedWorktree"), binding: bindings.advancedWorktree ?? "" },
{ label: t("agentManager.shortcuts.deleteWorktree"), binding: bindings.closeWorktree ?? "" },
{ label: t("agentManager.shortcuts.openWorktree"), binding: bindings.openWorktree ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.tabs"),
shortcuts: [
{ label: t("agentManager.shortcuts.previousTab"), binding: bindings.previousTab ?? "" },
{ label: t("agentManager.shortcuts.nextTab"), binding: bindings.nextTab ?? "" },
{ label: t("agentManager.shortcuts.newTab"), binding: bindings.newTab ?? "" },
{ label: t("agentManager.shortcuts.closeTab"), binding: bindings.closeTab ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.terminal"),
shortcuts: [
{ label: t("agentManager.shortcuts.toggleTerminal"), binding: bindings.showTerminal ?? "" },
{ label: t("agentManager.shortcuts.runScript"), binding: bindings.runScript ?? "" },
{ label: t("agentManager.shortcuts.toggleDiff"), binding: bindings.toggleDiff ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.global"),
shortcuts: [
{ label: t("agentManager.shortcuts.openAgentManager"), binding: bindings.agentManagerOpen ?? "" },
{ label: t("agentManager.shortcuts.cycleAgentMode"), binding: bindings.cycleAgentMode ?? "" },
{ label: t("agentManager.shortcuts.cyclePreviousAgentMode"), binding: bindings.cyclePreviousAgentMode ?? "" },
{ label: t("agentManager.shortcuts.showShortcuts"), binding: bindings.showShortcuts ?? "" },
].filter((s) => s.binding),
},
].filter((c) => c.shortcuts.length > 0)
}
import { parseBindingTokens } from "./keybind-tokens"
const AgentManagerContent: Component = () => {
@@ -1003,6 +934,27 @@ const AgentManagerContent: Component = () => {
setReviewActive(remembered === REVIEW_TAB_ID && reviewOpenByContext()[worktreeId] === true)
}
const addSessionToCurrentWorktree = (sid: string) => {
const sel = selection()
if (!sel || sel === LOCAL) return false
const current = managedSessions().find((entry) => entry.id === sid)
if (current?.worktreeId) return focusManagedSession(current.worktreeId, sid)
saveTabMemory()
setHistory(false)
setReviewActive(false)
appendToTabOrder(sel, sid)
evictLocal(sid)
vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel, sessionId: sid })
return true
}
const focusManagedSession = (worktreeId: string, sid: string) => {
selectWorktree(worktreeId)
setHistory(false)
session.selectSession(sid)
return true
}
const cycleAgent = (direction: 1 | -1) => {
const available = session.agents().filter((a) => a.mode !== "subagent" && !a.hidden)
if (available.length <= 1) return
@@ -1220,6 +1172,7 @@ const AgentManagerContent: Component = () => {
saveTabMemory()
appendToTabOrder(ev.worktreeId, ev.sessionId)
setSelection(ev.worktreeId)
evictLocal(ev.sessionId)
drafts.apply(ev.worktreeId, ev.sessionId)
session.selectSession(ev.sessionId)
}
@@ -2988,6 +2941,7 @@ const AgentManagerContent: Component = () => {
<Show when={history()}>
<HistoryView
onSelectSession={(id) => {
if (addSessionToCurrentWorktree(id)) return
setHistory(false)
if (localSessionIDs().includes(id)) {
saveTabMemory()
@@ -3021,6 +2975,7 @@ const AgentManagerContent: Component = () => {
<div class="am-chat-wrapper">
<ChatView
onSelectSession={(id) => {
if (addSessionToCurrentWorktree(id)) return
if (localSessionIDs().includes(id)) {
session.selectSession(id)
if (selection() === null) setSelection(LOCAL)
@@ -14,13 +14,22 @@ import type { DiffLineAnnotation, AnnotationSide, SelectedLineRange } from "@pie
import type { WorktreeFileDiff } from "../src/types/messages"
import { KILO_FILE_PATH_MIME } from "../src/utils/path-mentions"
import { useLanguage } from "../src/context/language"
import { useVSCode } from "../src/context/vscode"
import { useServer } from "../src/context/server"
import { useProvider } from "../src/context/provider"
import { useConfig } from "../src/context/config"
import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability"
import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText"
import { getDirectory, getFilename, lineCount, sanitizeReviewComments, type ReviewComment } from "./review-comments"
import {
buildFileAnnotations,
buildReviewAnnotation,
reviewDraftSpeechKey,
reviewEditSpeechKey,
type AnnotationLabels,
type AnnotationMeta,
} from "./review-annotations"
import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech"
import {
LONG_DIFF_MARKER_FILE_COUNT,
allOpenFiles,
@@ -59,6 +68,13 @@ interface DiffPanelProps {
export const DiffPanel: Component<DiffPanelProps> = (props) => {
const { t } = useLanguage()
const vscode = useVSCode()
const server = useServer()
const provider = useProvider()
const { config, settings } = useConfig()
const speech = useSpeechToText(vscode, server, { t })
const canUseSpeech = () => canUseSpeechToText(settings(), config(), provider.connected(), server.profileData())
const speechModel = () => selectedSpeechToTextModel(settings())
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
const sendAllKeybind = () =>
isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other")
@@ -78,6 +94,21 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
null,
)
const [editing, setEditing] = createSignal<string | null>(null)
const speechKeys = createMemo(() => {
const keys = new Set<string>()
const current = draft()
const edit = editing()
if (current) keys.add(reviewDraftSpeechKey(current))
if (edit) keys.add(reviewEditSpeechKey(edit))
return keys
})
const reviewSpeech = createReviewAnnotationSpeechRenderer({
speech,
enabled: canUseSpeech,
model: speechModel,
label: t,
keys: speechKeys,
})
let nextId = 0
// Tracks the session key for which initial open state has already run. When the
// key changes (different worktree) we expand reviewable files. Within the same key,
@@ -307,6 +338,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
cancelDraft,
labels: labels(),
activeTerminalId: props.activeTerminalId,
speech: reviewSpeech,
})
}
@@ -20,15 +20,24 @@ import type { DiffLineAnnotation, AnnotationSide, SelectedLineRange } from "@pie
import type { WorktreeFileDiff } from "../src/types/messages"
import { KILO_FILE_PATH_MIME } from "../src/utils/path-mentions"
import { useLanguage } from "../src/context/language"
import { useVSCode } from "../src/context/vscode"
import { useServer } from "../src/context/server"
import { useProvider } from "../src/context/provider"
import { useConfig } from "../src/context/config"
import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability"
import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText"
import { FileTree } from "./FileTree"
import { treeOrder } from "./file-tree-utils"
import { getDirectory, getFilename, lineCount, sanitizeReviewComments, type ReviewComment } from "./review-comments"
import {
buildFileAnnotations,
buildReviewAnnotation,
reviewDraftSpeechKey,
reviewEditSpeechKey,
type AnnotationLabels,
type AnnotationMeta,
} from "./review-annotations"
import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech"
import {
LONG_DIFF_MARKER_FILE_COUNT,
allOpenFiles,
@@ -69,6 +78,13 @@ interface FullScreenDiffViewProps {
export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) => {
const { t } = useLanguage()
const vscode = useVSCode()
const server = useServer()
const provider = useProvider()
const { config, settings } = useConfig()
const speech = useSpeechToText(vscode, server, { t })
const canUseSpeech = () => canUseSpeechToText(settings(), config(), provider.connected(), server.profileData())
const speechModel = () => selectedSpeechToTextModel(settings())
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
const sendAllKeybind = () =>
isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other")
@@ -88,6 +104,21 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
null,
)
const [editing, setEditing] = createSignal<string | null>(null)
const speechKeys = createMemo(() => {
const keys = new Set<string>()
const current = draft()
const edit = editing()
if (current) keys.add(reviewDraftSpeechKey(current))
if (edit) keys.add(reviewEditSpeechKey(edit))
return keys
})
const reviewSpeech = createReviewAnnotationSpeechRenderer({
speech,
enabled: canUseSpeech,
model: speechModel,
label: t,
keys: speechKeys,
})
const [activeFile, setActiveFile] = createSignal<string | null>(null)
const [treeWidth, setTreeWidth] = createSignal(240)
let nextId = 0
@@ -322,6 +353,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
cancelDraft,
labels: labels(),
activeTerminalId: props.activeTerminalId,
speech: reviewSpeech,
})
}
@@ -2146,6 +2146,11 @@ body.am-wt-dragging-active * {
gap: 6px;
}
.am-annotation-speech {
display: flex;
align-items: center;
}
.am-annotation-btn {
padding: 2px 10px;
font-size: inherit;
@@ -0,0 +1,138 @@
import { createEffect, createRoot, createSignal, onCleanup, Show, type Accessor } from "solid-js"
import { render as renderSolid } from "solid-js/web"
import { SpeechToTextButton } from "../src/components/speech-to-text/SpeechToTextButton"
import { insertSpacedText } from "../src/components/chat/prompt-input-utils"
import type { SpeechState, SpeechToText } from "../src/components/speech-to-text/useSpeechToText"
import { reviewAnnotationSpeechKey, type AnnotationMeta } from "./review-annotations"
type Props = {
speech: SpeechToText
enabled: Accessor<boolean>
model: Accessor<string>
label: (key: string) => string
keys: Accessor<Set<string>>
}
type Node = {
host: HTMLDivElement
dispose: VoidFunction
setTextarea: (textarea: HTMLTextAreaElement) => void
}
function insertReviewSpeechText(textarea: HTMLTextAreaElement, value: string): void {
const text = textarea.value
const start = textarea.selectionStart ?? text.length
const end = textarea.selectionEnd ?? start
const result = insertSpacedText(text, value, start, end)
textarea.value = result.text
textarea.setSelectionRange(result.pos, result.pos)
textarea.focus()
}
export function createReviewAnnotationSpeechRenderer(props: Props) {
const nodes = new Map<string, Node>()
const [owner, setOwner] = createSignal<string | undefined>()
const mount = (key: string, textarea: HTMLTextAreaElement): Node | undefined => {
if (typeof document === "undefined") return undefined
const host = document.createElement("div")
host.className = "am-annotation-speech"
let field = textarea
const mine = () => owner() === key
const state = (): SpeechState => (mine() ? props.speech.state() : "idle")
const start = (model: string) => {
setOwner(key)
props.speech.start({
model,
insert: (value) => insertReviewSpeechText(field, value),
})
}
const speech: SpeechToText = {
state,
error: () => (mine() ? props.speech.error() : undefined),
active: () => mine() && props.speech.active(),
start: (opts) => start(opts.model),
stop: () => {
if (!mine()) return
props.speech.stop()
},
cancel: () => {
if (!mine()) return
props.speech.cancel()
setOwner(undefined)
},
clear: () => {
if (!mine()) return
props.speech.clear()
setOwner(undefined)
},
}
const blocked = () => props.speech.active() && !mine()
const dispose = createRoot((root) => {
const view = renderSolid(
() => (
<Show when={props.enabled()}>
<SpeechToTextButton
speech={speech}
disabled={blocked()}
start={() => start(props.model())}
label={props.label}
/>
</Show>
),
host,
)
onCleanup(view)
return root
})
const node = {
host,
dispose,
setTextarea: (next: HTMLTextAreaElement) => {
field = next
},
}
nodes.set(key, node)
return node
}
const render = (meta: AnnotationMeta, textarea: HTMLTextAreaElement): HTMLElement | undefined => {
const key = reviewAnnotationSpeechKey(meta)
if (!key) return undefined
const node = nodes.get(key) ?? mount(key, textarea)
if (!node) return undefined
node.setTextarea(textarea)
return node.host
}
createEffect(() => {
const keys = props.keys()
const current = owner()
if (current && !keys.has(current)) {
if (props.speech.active()) props.speech.cancel()
if (props.speech.state() === "error") props.speech.clear()
setOwner(undefined)
}
for (const [key, node] of nodes) {
if (keys.has(key)) continue
node.dispose()
nodes.delete(key)
}
})
onCleanup(() => {
if (owner() && props.speech.active()) props.speech.cancel()
for (const [, node] of nodes) node.dispose()
nodes.clear()
})
return {
active: props.speech.active,
render,
}
}
@@ -26,6 +26,22 @@ export interface AnnotationMeta {
editing?: boolean
}
type SpeechDraft = Pick<AnnotationMeta, "file" | "side" | "line" | "endLine">
export function reviewDraftSpeechKey(draft: SpeechDraft): string {
return `draft:${draft.file}:${draft.side}:${draft.line}:${draft.endLine ?? draft.line}`
}
export function reviewEditSpeechKey(id: string): string {
return `edit:${id}`
}
export function reviewAnnotationSpeechKey(meta: AnnotationMeta): string | undefined {
if (meta.type === "draft") return reviewDraftSpeechKey(meta)
if (!meta.editing || !meta.comment) return undefined
return reviewEditSpeechKey(meta.comment.id)
}
interface AnnotationHandlers {
diffs: WorktreeFileDiff[]
editing: string | null
@@ -36,6 +52,10 @@ interface AnnotationHandlers {
cancelDraft: () => void
labels: AnnotationLabels
activeTerminalId?: string
speech?: {
active: () => boolean
render: (meta: AnnotationMeta, textarea: HTMLTextAreaElement) => HTMLElement | undefined
}
}
function focusWhenConnected(el: HTMLTextAreaElement): void {
@@ -150,6 +170,8 @@ export function buildReviewAnnotation(
submitButton.className = "am-annotation-btn am-annotation-btn-submit"
submitButton.textContent = handlers.labels.comment
const speech = handlers.speech?.render(meta, textarea)
if (speech) actions.appendChild(speech)
actions.appendChild(cancelButton)
actions.appendChild(submitButton)
wrapper.appendChild(header)
@@ -159,6 +181,7 @@ export function buildReviewAnnotation(
focusWhenConnected(textarea)
const submit = () => {
if (handlers.speech?.active()) return
const text = textarea.value.trim()
if (!text) return
const diff = handlers.diffs.find((item) => item.file === meta.file)
@@ -216,6 +239,8 @@ export function buildReviewAnnotation(
saveButton.className = "am-annotation-btn am-annotation-btn-submit"
saveButton.textContent = handlers.labels.save
const speech = handlers.speech?.render(meta, textarea)
if (speech) actions.appendChild(speech)
actions.appendChild(cancelButton)
actions.appendChild(saveButton)
wrapper.appendChild(header)
@@ -231,6 +256,7 @@ export function buildReviewAnnotation(
saveButton.addEventListener("click", (event) => {
event.stopPropagation()
if (handlers.speech?.active()) return
const text = textarea.value.trim()
if (!text) return
handlers.updateComment(comment.id, text)
@@ -244,6 +270,7 @@ export function buildReviewAnnotation(
}
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault()
if (handlers.speech?.active()) return
const text = textarea.value.trim()
if (!text) return
handlers.updateComment(comment.id, text)
@@ -0,0 +1,67 @@
export interface ShortcutEntry {
label: string
binding: string
}
export interface ShortcutCategory {
title: string
shortcuts: ShortcutEntry[]
}
export function buildShortcutCategories(
bindings: Record<string, string>,
t: (key: string, params?: Record<string, string | number>) => string,
): ShortcutCategory[] {
return [
{
title: t("agentManager.shortcuts.category.quickSwitch"),
shortcuts: [
{
label: t("agentManager.shortcuts.jumpToItem"),
binding: (() => {
const first = bindings.jumpTo1 ?? ""
const prefix = first.replace(/\d+$/, "")
return prefix ? `${prefix}1-9` : ""
})(),
},
],
},
{
title: t("agentManager.shortcuts.category.sidebar"),
shortcuts: [
{ label: t("agentManager.shortcuts.previousItem"), binding: bindings.previousSession ?? "" },
{ label: t("agentManager.shortcuts.nextItem"), binding: bindings.nextSession ?? "" },
{ label: t("agentManager.shortcuts.newWorktree"), binding: bindings.newWorktree ?? "" },
{ label: t("agentManager.shortcuts.advancedWorktree"), binding: bindings.advancedWorktree ?? "" },
{ label: t("agentManager.shortcuts.deleteWorktree"), binding: bindings.closeWorktree ?? "" },
{ label: t("agentManager.shortcuts.openWorktree"), binding: bindings.openWorktree ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.tabs"),
shortcuts: [
{ label: t("agentManager.shortcuts.previousTab"), binding: bindings.previousTab ?? "" },
{ label: t("agentManager.shortcuts.nextTab"), binding: bindings.nextTab ?? "" },
{ label: t("agentManager.shortcuts.newTab"), binding: bindings.newTab ?? "" },
{ label: t("agentManager.shortcuts.closeTab"), binding: bindings.closeTab ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.terminal"),
shortcuts: [
{ label: t("agentManager.shortcuts.toggleTerminal"), binding: bindings.showTerminal ?? "" },
{ label: t("agentManager.shortcuts.runScript"), binding: bindings.runScript ?? "" },
{ label: t("agentManager.shortcuts.toggleDiff"), binding: bindings.toggleDiff ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.global"),
shortcuts: [
{ label: t("agentManager.shortcuts.openAgentManager"), binding: bindings.agentManagerOpen ?? "" },
{ label: t("agentManager.shortcuts.cycleAgentMode"), binding: bindings.cycleAgentMode ?? "" },
{ label: t("agentManager.shortcuts.cyclePreviousAgentMode"), binding: bindings.cyclePreviousAgentMode ?? "" },
{ label: t("agentManager.shortcuts.showShortcuts"), binding: bindings.showShortcuts ?? "" },
].filter((s) => s.binding),
},
].filter((c) => c.shortcuts.length > 0)
}
@@ -335,8 +335,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const speechModel = () => selectedSpeechToTextModel(settings())
const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0
const canSend = () =>
hasInput() && !isDisabled() && !speech.active() && !terminal.pending() && !git.pending() && !props.blocked?.()
const showStop = () => isBusy() && !hasInput()
!isDisabled() &&
!terminal.pending() &&
!git.pending() &&
!props.blocked?.() &&
(speech.state() === "recording" || (hasInput() && !speech.active()))
const sendLabel = () => {
if (props.blocked?.()) return language.t("prompt.action.send.blocked")
if (speech.state() === "recording") return language.t("prompt.action.send.recording")
return language.t("prompt.action.send")
}
const showStop = () => isBusy() && !hasInput() && speech.state() !== "recording"
const isAtEnd = () =>
textareaRef ? atEnd(textareaRef.selectionStart, textareaRef.selectionEnd, textareaRef.value.length) : false
const highlightMentions = () => {
@@ -675,6 +684,33 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
speech.start({ model: speechModel(), insert: insertSpeechText })
}
const transcribeAndSend = () => {
const key = draftKey()
const id = sid()
const context = ctx()
const value = text()
const comments = reviewComments()
const images = imageAttach.images()
speech.stop({
done: () => void handleSend(),
ready: () =>
draftKey() === key &&
sid() === id &&
ctx() === context &&
text() === value &&
reviewComments() === comments &&
imageAttach.images() === images,
})
}
const handleSendClick = () => {
if (speech.state() !== "recording" || !canSend()) {
void handleSend()
return
}
transcribeAndSend()
}
const handleSend = async () => {
const draft = text().trim()
@@ -721,6 +757,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const pendingId = props.pendingSessionID ?? session.draftSessionID()
const id = sid()
const sel = session.selected(id)
const context = ctx()
const key = draftKey()
const terminalFile = await terminal.resolveAttachment(message, id).catch((err: Error) => {
showToast({ variant: "error", title: "Terminal context unavailable", description: err.message })
@@ -742,16 +780,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
]
const attachments = allFiles.length > 0 ? allFiles : undefined
const key = draftKey()
// Server-side slash command (cmdMatch/matched already computed above)
if (matched) {
const rest = draft.slice(cmdMatch![0].length).trim()
const args = review && rest ? `${review}\n\n${rest}` : rest || review
session.sendCommand(matched.name, args, sel?.providerID, sel?.modelID, attachments, pendingId, ctx())
session.sendCommand(matched.name, args, sel?.providerID, sel?.modelID, attachments, pendingId, context)
} else {
session.sendMessage(message, sel?.providerID, sel?.modelID, attachments, pendingId, ctx())
session.sendMessage(message, sel?.providerID, sel?.modelID, attachments, pendingId, context)
}
drafts.delete(key)
reviewDrafts.delete(key)
imageDrafts.delete(key)
if (draftKey() !== key) return
history.append(draft)
history.reset()
setText("")
@@ -759,9 +801,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
imageAttach.clear()
mention.closeMention()
slash.close()
drafts.delete(key)
reviewDrafts.delete(key)
imageDrafts.delete(key)
if (textareaRef) textareaRef.style.height = "auto"
}
@@ -1085,16 +1124,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Show
when={showStop()}
fallback={
<Tooltip
value={props.blocked?.() ? language.t("prompt.action.send.blocked") : language.t("prompt.action.send")}
placement="top"
>
<Tooltip value={sendLabel()} placement="top">
<Button
variant="ghost"
size="small"
onClick={() => void handleSend()}
onClick={handleSendClick}
aria-disabled={!canSend()}
aria-label={language.t("prompt.action.send")}
aria-label={sendLabel()}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M1.5 1.5L14.5 8L1.5 14.5V9L10 8L1.5 7V1.5Z" />
@@ -26,12 +26,17 @@ type StartOptions = {
insert: InsertTranscript
}
type StopOptions = {
done?: () => void
ready?: () => boolean
}
export type SpeechToText = {
state: Accessor<SpeechState>
error: Accessor<string | undefined>
active: Accessor<boolean>
start: (opts: StartOptions) => void
stop: () => void
stop: (opts?: StopOptions) => void
cancel: () => void
clear: () => void
}
@@ -45,6 +50,8 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe
let request = ""
let counter = 0
let insert: InsertTranscript | undefined
let done: (() => void) | undefined
let ready: (() => boolean) | undefined
const unsub = vscode.onMessage((msg) => {
if (!isSpeechMessage(msg)) return
@@ -73,10 +80,12 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe
return
}
const next = ready?.() === false ? undefined : done
insert?.(text)
cleanup()
setState("idle")
setError(undefined)
next?.()
})
onCleanup(() => {
@@ -113,8 +122,10 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe
})
}
function stop() {
function stop(opts?: StopOptions) {
if (state() !== "recording") return
done = opts?.done
ready = opts?.ready
setState("transcribing")
vscode.postMessage({ type: "speechToTextStop", requestId: request })
}
@@ -143,6 +154,8 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe
function cleanup() {
request = ""
insert = undefined
done = undefined
ready = undefined
}
return { state, error, active, start, stop, cancel, clear }
+1
View File
@@ -268,6 +268,7 @@ export const dict = {
"prompt.attachment.remove": "إزالة المرفق",
"prompt.action.send": "إرسال",
"prompt.action.send.blocked": "أجب عن السؤال المعلق أو تجاهله أولاً",
"prompt.action.send.recording": "تفريغ وإرسال",
"prompt.action.stop": "توقف",
"prompt.action.enhance": "تحسين النص",
"prompt.action.autoApprove.enable": "تفعيل الموافقة التلقائية",
+1
View File
@@ -269,6 +269,7 @@ export const dict = {
"prompt.attachment.remove": "Remover anexo",
"prompt.action.send": "Enviar",
"prompt.action.send.blocked": "Responda ou feche a pergunta pendente primeiro",
"prompt.action.send.recording": "Transcrever e enviar",
"prompt.action.stop": "Parar",
"prompt.action.enhance": "Melhorar prompt",
"prompt.action.autoApprove.enable": "Ativar aprovação automática",
+1
View File
@@ -270,6 +270,7 @@ export const dict = {
"prompt.attachment.remove": "Ukloni prilog",
"prompt.action.send": "Pošalji",
"prompt.action.send.blocked": "Prvo odgovorite ili odbacite pitanje na čekanju",
"prompt.action.send.recording": "Transkribuj i pošalji",
"prompt.action.stop": "Zaustavi",
"prompt.action.enhance": "Poboljšaj prompt",
"prompt.action.autoApprove.enable": "Uključi automatsko odobravanje",
+1
View File
@@ -269,6 +269,7 @@ export const dict = {
"prompt.attachment.remove": "Fjern vedhæftning",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Besvar eller afvis det afventende spørgsmål først",
"prompt.action.send.recording": "Transskriber og send",
"prompt.action.stop": "Stop",
"prompt.action.enhance": "Forbedr prompt",
"prompt.action.autoApprove.enable": "Aktiver automatisk godkendelse",
+1
View File
@@ -273,6 +273,7 @@ export const dict = {
"prompt.attachment.remove": "Anhang entfernen",
"prompt.action.send": "Senden",
"prompt.action.send.blocked": "Beantworten oder verwerfen Sie zuerst die ausstehende Frage",
"prompt.action.send.recording": "Transkribieren und senden",
"prompt.action.stop": "Stopp",
"prompt.action.enhance": "Prompt verbessern",
"prompt.action.autoApprove.enable": "Automatische Genehmigung aktivieren",
@@ -270,6 +270,7 @@ export const dict = {
"prompt.attachment.remove": "Remove attachment",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Answer or dismiss the pending question first",
"prompt.action.send.recording": "Transcribe and send",
"prompt.action.stop": "Stop",
"prompt.action.enhance": "Enhance prompt",
"prompt.action.indexing": "Indexing settings",
+1
View File
@@ -270,6 +270,7 @@ export const dict = {
"prompt.attachment.remove": "Eliminar adjunto",
"prompt.action.send": "Enviar",
"prompt.action.send.blocked": "Responda o descarte la pregunta pendiente primero",
"prompt.action.send.recording": "Transcribir y enviar",
"prompt.action.stop": "Detener",
"prompt.action.enhance": "Mejorar prompt",
"prompt.action.autoApprove.enable": "Activar aprobación automática",
+1
View File
@@ -271,6 +271,7 @@ export const dict = {
"prompt.attachment.remove": "Supprimer la pièce jointe",
"prompt.action.send": "Envoyer",
"prompt.action.send.blocked": "Répondez ou rejetez d'abord la question en attente",
"prompt.action.send.recording": "Transcrire et envoyer",
"prompt.action.stop": "Arrêter",
"prompt.action.enhance": "Améliorer le prompt",
"prompt.action.autoApprove.enable": "Activer l'approbation automatique",
+1
View File
@@ -268,6 +268,7 @@ export const dict = {
"prompt.attachment.remove": "添付ファイルを削除",
"prompt.action.send": "送信",
"prompt.action.send.blocked": "最初に保留中の質問に答えるか、閉じてください",
"prompt.action.send.recording": "文字起こしして送信",
"prompt.action.stop": "停止",
"prompt.action.enhance": "プロンプトを改善",
"prompt.action.autoApprove.enable": "自動承認を有効化",
+1
View File
@@ -271,6 +271,7 @@ export const dict = {
"prompt.attachment.remove": "첨부 파일 제거",
"prompt.action.send": "전송",
"prompt.action.send.blocked": "먼저 대기 중인 질문에 답하거나 닫아주세요",
"prompt.action.send.recording": "텍스트 변환 및 전송",
"prompt.action.stop": "중지",
"prompt.action.enhance": "프롬프트 개선",
"prompt.action.autoApprove.enable": "자동 승인 사용",
+1
View File
@@ -270,6 +270,7 @@ export const dict = {
"prompt.attachment.remove": "Bijlage verwijderen",
"prompt.action.send": "Verzenden",
"prompt.action.send.blocked": "Beantwoord of negeer eerst de openstaande vraag",
"prompt.action.send.recording": "Transcriberen en verzenden",
"prompt.action.stop": "Stop",
"prompt.action.enhance": "Prompt verbeteren",
"prompt.action.indexing": "Indexeringsinstellingen",
+1
View File
@@ -272,6 +272,7 @@ export const dict = {
"prompt.attachment.remove": "Fjern vedlegg",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Svar på eller avvis det ventende spørsmålet først",
"prompt.action.send.recording": "Transkriber og send",
"prompt.action.stop": "Stopp",
"prompt.action.enhance": "Forbedre prompt",
"prompt.action.autoApprove.enable": "Aktiver automatisk godkjenning",
+1
View File
@@ -269,6 +269,7 @@ export const dict = {
"prompt.attachment.remove": "Usuń załącznik",
"prompt.action.send": "Wyślij",
"prompt.action.send.blocked": "Najpierw odpowiedz na oczekujące pytanie lub je odrzuć",
"prompt.action.send.recording": "Transkrybuj i wyślij",
"prompt.action.stop": "Zatrzymaj",
"prompt.action.enhance": "Ulepsz prompt",
"prompt.action.autoApprove.enable": "Włącz automatyczne zatwierdzanie",
+1
View File
@@ -268,6 +268,7 @@ export const dict = {
"prompt.attachment.remove": "Удалить вложение",
"prompt.action.send": "Отправить",
"prompt.action.send.blocked": "Сначала ответьте на ожидающий вопрос или отклоните его",
"prompt.action.send.recording": "Расшифровать и отправить",
"prompt.action.stop": "Остановить",
"prompt.action.enhance": "Улучшить промпт",
"prompt.action.autoApprove.enable": "Включить автоодобрение",
+1
View File
@@ -269,6 +269,7 @@ export const dict = {
"prompt.attachment.remove": "เอาไฟล์แนบออก",
"prompt.action.send": "ส่ง",
"prompt.action.send.blocked": "โปรดตอบหรือข้ามคำถามที่รอดำเนินการก่อน",
"prompt.action.send.recording": "ถอดเสียงและส่ง",
"prompt.action.stop": "หยุด",
"prompt.action.enhance": "ปรับปรุงพรอมต์",
"prompt.action.autoApprove.enable": "เปิดใช้การอนุมัติอัตโนมัติ",
+1
View File
@@ -269,6 +269,7 @@ export const dict = {
"prompt.attachment.remove": "Eki kaldır",
"prompt.action.send": "Gönder",
"prompt.action.send.blocked": "Bekleyen soruyu önce yanıtlayın veya kapatın",
"prompt.action.send.recording": "Yazıya dök ve gönder",
"prompt.action.stop": "Durdur",
"prompt.action.enhance": "Komutu geliştir",
"prompt.action.indexing": "İndeksleme ayarları",
+1
View File
@@ -270,6 +270,7 @@ export const dict = {
"prompt.attachment.remove": "Видалити вкладення",
"prompt.action.send": "Надіслати",
"prompt.action.send.blocked": "Спочатку дайте відповідь або закрийте очікуюче питання",
"prompt.action.send.recording": "Транскрибувати та надіслати",
"prompt.action.stop": "Зупинити",
"prompt.action.enhance": "Покращити запит",
"prompt.action.indexing": "Налаштування індексування",
+1
View File
@@ -273,6 +273,7 @@ export const dict = {
"prompt.attachment.remove": "移除附件",
"prompt.action.send": "发送",
"prompt.action.send.blocked": "请先回答或忽略待处理的问题",
"prompt.action.send.recording": "转录并发送",
"prompt.action.stop": "停止",
"prompt.action.enhance": "优化提示词",
"prompt.action.resetModel": "重置模型为默认值",
+1
View File
@@ -268,6 +268,7 @@ export const dict = {
"prompt.attachment.remove": "移除附件",
"prompt.action.send": "傳送",
"prompt.action.send.blocked": "請先回答或忽略待處理的問題",
"prompt.action.send.recording": "轉錄並傳送",
"prompt.action.stop": "停止",
"prompt.action.enhance": "改善提示詞",
"prompt.action.autoApprove.enable": "啟用自動核准",
@@ -29,6 +29,7 @@ export interface SendMessageRequest {
variant?: string
files?: FileAttachment[]
agentManagerContext?: string
contextDirectory?: string
}
export interface AbortRequest {
@@ -240,6 +241,7 @@ export interface SendCommandRequest {
variant?: string
files?: FileAttachment[]
agentManagerContext?: string
contextDirectory?: string
}
export interface RemoveSkillMessage {
@@ -500,6 +502,7 @@ export interface OpenLocallyRequest {
export interface AddSessionToWorktreeRequest {
type: "agentManager.addSessionToWorktree"
worktreeId: string
sessionId?: string
}
// Fork an existing session (copies conversation history)