mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge remote-tracking branch 'origin/main' into fix/agent-manager-attribution
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep continued Agent Manager sessions in the selected worktree instead of moving them to Local.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support speech-to-text voice input in Agent Manager inline review comments.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Send recorded voice prompts directly when the prompt send button is pressed.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user