mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
Merge branch 'main' into fix/cursor-pointer-clickable-elements
This commit is contained in:
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -17,6 +17,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
close-stale-prs:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404 # kilocode_change
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
|
||||
@@ -13,7 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
close-non-compliant:
|
||||
if: false # kilocode_change - disabled: not needed in kilocode repo
|
||||
if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Close non-compliant issues and PRs after 2 hours
|
||||
|
||||
@@ -14,6 +14,7 @@ on:
|
||||
|
||||
jobs:
|
||||
link-checker:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -16,6 +16,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
nix-eval:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
# Native runners required: bun install cross-compilation flags (--os/--cpu)
|
||||
# do not produce byte-identical node_modules as native installs.
|
||||
compute-hash:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -76,7 +77,7 @@ jobs:
|
||||
|
||||
update-hashes:
|
||||
needs: compute-hash
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.repository == 'Kilo-Org/kilocode' && github.event_name != 'pull_request'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
# kilocode_change start
|
||||
check-author:
|
||||
if: false # kilocode_change - disabled: not needed in kilocode repo
|
||||
if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo
|
||||
uses: ./.github/workflows/check-org-member.yml
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
check-duplicates:
|
||||
needs: check-author
|
||||
if: false # kilocode_change - disabled: not needed in kilocode repo
|
||||
if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo
|
||||
# kilocode_change end
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
fi
|
||||
|
||||
add-contributor-label:
|
||||
if: false # kilocode_change - disabled: not needed in kilocode repo
|
||||
if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404 # kilocode_change
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
# kilocode_change start
|
||||
check-author:
|
||||
if: false # kilocode_change - disabled: not needed in kilocode repo
|
||||
if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo
|
||||
uses: ./.github/workflows/check-org-member.yml
|
||||
with:
|
||||
username: ${{ github.event.pull_request.user.login }}
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
check-standards:
|
||||
needs: check-author
|
||||
if: false # kilocode_change - disabled: not needed in kilocode repo
|
||||
if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo
|
||||
# kilocode_change end
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
console.log('PR meets all standards');
|
||||
|
||||
check-compliance:
|
||||
if: false # kilocode_change - disabled: not needed in kilocode repo
|
||||
if: github.repository == 'Kilo-Org/kilocode' && false # kilocode_change - disabled: not needed in kilocode repo
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -11,6 +11,7 @@ env:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
if: github.repository == 'Kilo-Org/kilocode'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -2,18 +2,33 @@ name: Visual Regression Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/kilo-ui/**"
|
||||
- "packages/ui/**"
|
||||
- "packages/util/**"
|
||||
- "packages/sdk/js/**"
|
||||
- "packages/kilo-vscode/webview-ui/**"
|
||||
- "packages/kilo-vscode/.storybook/**"
|
||||
- "packages/kilo-vscode/tests/visual-regression*"
|
||||
- ".github/workflows/visual-regression.yml"
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
check-paths:
|
||||
name: Check changed paths
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matched: ${{ steps.filter.outputs.matched }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Kilo-Org/paths-filter@master
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
matched:
|
||||
- "packages/kilo-ui/**"
|
||||
- "packages/ui/**"
|
||||
- "packages/util/**"
|
||||
- "packages/sdk/js/**"
|
||||
- "packages/kilo-vscode/webview-ui/**"
|
||||
- "packages/kilo-vscode/.storybook/**"
|
||||
- "packages/kilo-vscode/tests/visual-regression*"
|
||||
- ".github/workflows/visual-regression.yml"
|
||||
|
||||
visual-regression:
|
||||
needs: check-paths
|
||||
if: needs.check-paths.outputs.matched == 'true'
|
||||
name: Visual Regression (kilo-ui)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 15
|
||||
@@ -23,7 +38,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.BOT_PAT }}
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Setup Bun
|
||||
@@ -77,6 +92,8 @@ jobs:
|
||||
|
||||
- name: Commit and push new baselines (if any)
|
||||
id: commit-baselines
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BOT_PAT }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
@@ -106,6 +123,8 @@ jobs:
|
||||
retention-days: 7
|
||||
|
||||
visual-regression-vscode:
|
||||
needs: check-paths
|
||||
if: needs.check-paths.outputs.matched == 'true'
|
||||
name: Visual Regression (kilo-vscode webview)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 15
|
||||
@@ -115,7 +134,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.BOT_PAT }}
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: Setup Bun
|
||||
@@ -169,6 +188,8 @@ jobs:
|
||||
|
||||
- name: Commit and push new baselines (if any)
|
||||
id: commit-baselines-vscode
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.BOT_PAT }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
@@ -19,6 +19,7 @@ type Validator = any
|
||||
type Resolver = any
|
||||
type Errors = any
|
||||
type Auth = any
|
||||
type ModelCache = { clear: (providerID: string) => void }
|
||||
type Z = any
|
||||
|
||||
interface KiloRoutesDeps extends ImportDeps {
|
||||
@@ -28,6 +29,7 @@ interface KiloRoutesDeps extends ImportDeps {
|
||||
resolver: Resolver
|
||||
errors: Errors
|
||||
Auth: Auth
|
||||
ModelCache: ModelCache
|
||||
z: Z
|
||||
}
|
||||
|
||||
@@ -72,6 +74,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
Bus,
|
||||
SessionCreatedEvent,
|
||||
Identifier,
|
||||
ModelCache,
|
||||
} = deps
|
||||
|
||||
const Organization = z.object({
|
||||
@@ -200,6 +203,8 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
|
||||
...(organizationId && { accountId: organizationId }),
|
||||
})
|
||||
|
||||
ModelCache.clear("kilo")
|
||||
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ html[data-theme="kilo-vscode"] [data-component="tool-trigger"] {
|
||||
}
|
||||
|
||||
/* Card styling for tool calls in the VS Code sidebar theme */
|
||||
html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"] {
|
||||
html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-type="tool"] {
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-weak-base, var(--vscode-panel-border));
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1612,6 +1612,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
|
||||
console.log("[Kilo New] KiloProvider: 🔐 Login successful")
|
||||
|
||||
await this.client.global.dispose().catch((e: unknown) =>
|
||||
console.warn("[Kilo New] KiloProvider: global.dispose() after login failed:", e),
|
||||
)
|
||||
|
||||
// Step 4: Fetch profile and push to webview
|
||||
const { data: profileData } = await this.client.kilo.profile(undefined, { throwOnError: true })
|
||||
this.postMessage({ type: "profileData", data: profileData })
|
||||
@@ -1657,6 +1661,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
return
|
||||
}
|
||||
|
||||
await this.client.global.dispose().catch((e: unknown) =>
|
||||
console.warn("[Kilo New] KiloProvider: global.dispose() after org switch failed:", e),
|
||||
)
|
||||
|
||||
// Org switch succeeded — refresh profile and providers independently (best-effort)
|
||||
try {
|
||||
const profileResult = await sdkClient.kilo.profile()
|
||||
@@ -1709,6 +1717,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
type: "profileData",
|
||||
data: null,
|
||||
})
|
||||
|
||||
await this.client.global.dispose().catch((e: unknown) =>
|
||||
console.warn("[Kilo New] KiloProvider: global.dispose() after logout failed:", e),
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: ❌ Logout failed:", error)
|
||||
this.postMessage({
|
||||
@@ -1716,6 +1729,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
message: getErrorMessage(error) || "Failed to logout",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1802,6 +1817,21 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
return this.connectionService.resolveEventSessionId(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-fetch all server-side state after an auth change (login/logout/org switch).
|
||||
* After instance.dispose() clears the server cache, the next request to each
|
||||
* endpoint will re-initialize with the current auth state.
|
||||
* This mirrors the TUI's sync.bootstrap() pattern.
|
||||
*/
|
||||
private async reloadAfterAuthChange(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.fetchAndSendProviders(),
|
||||
this.fetchAndSendAgents(),
|
||||
this.fetchAndSendConfig(),
|
||||
this.fetchAndSendNotifications(),
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle SSE events from the CLI backend.
|
||||
* Filters events by project ID and tracked session IDs so each webview only sees its own sessions.
|
||||
@@ -1827,8 +1857,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
|
||||
// Refresh provider and agent lists when the server signals a state disposal
|
||||
if (event.type === "server.instance.disposed" || event.type === "global.disposed") {
|
||||
void this.fetchAndSendProviders()
|
||||
void this.fetchAndSendAgents()
|
||||
void this.reloadAfterAuthChange();
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -67,11 +67,45 @@ export class WorktreeManager {
|
||||
this.log = log
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-project git operation mutex
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Serializes git-writing operations per repository root so concurrent
|
||||
// callers (e.g. multi-version worktree creation) don't hit index.lock
|
||||
// conflicts. Operations on different repositories proceed in parallel.
|
||||
private static locks = new Map<string, Promise<void>>()
|
||||
|
||||
private withGitLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const key = this.root
|
||||
const prev = WorktreeManager.locks.get(key) ?? Promise.resolve()
|
||||
const result = prev.then(fn)
|
||||
const barrier = result.then(
|
||||
() => {},
|
||||
() => {},
|
||||
)
|
||||
WorktreeManager.locks.set(key, barrier)
|
||||
return result
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API (acquires git lock)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async createWorktree(params: {
|
||||
prompt?: string
|
||||
existingBranch?: string
|
||||
baseBranch?: string
|
||||
branchName?: string
|
||||
}): Promise<CreateWorktreeResult> {
|
||||
return this.withGitLock(() => this.createWorktreeImpl(params))
|
||||
}
|
||||
|
||||
private async createWorktreeImpl(params: {
|
||||
prompt?: string
|
||||
existingBranch?: string
|
||||
baseBranch?: string
|
||||
branchName?: string
|
||||
}): Promise<CreateWorktreeResult> {
|
||||
const repo = await this.git.checkIsRepo()
|
||||
if (!repo)
|
||||
@@ -108,7 +142,7 @@ export class WorktreeManager {
|
||||
|
||||
if (fs.existsSync(worktreePath)) {
|
||||
this.log(`Worktree directory exists, cleaning up before re-creation: ${worktreePath}`)
|
||||
await this.removeWorktree(worktreePath)
|
||||
await this.removeWorktreeImpl(worktreePath)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -154,6 +188,10 @@ export class WorktreeManager {
|
||||
* that git doesn't know about.
|
||||
*/
|
||||
async removeWorktree(worktreePath: string): Promise<void> {
|
||||
return this.withGitLock(() => this.removeWorktreeImpl(worktreePath))
|
||||
}
|
||||
|
||||
private async removeWorktreeImpl(worktreePath: string): Promise<void> {
|
||||
const clean = await this.git.raw(["worktree", "remove", worktreePath]).then(
|
||||
() => true,
|
||||
() => false,
|
||||
@@ -440,6 +478,10 @@ export class WorktreeManager {
|
||||
}
|
||||
|
||||
async createFromPR(url: string): Promise<CreateWorktreeResult> {
|
||||
return this.withGitLock(() => this.createFromPRImpl(url))
|
||||
}
|
||||
|
||||
private async createFromPRImpl(url: string): Promise<CreateWorktreeResult> {
|
||||
const parsed = parsePRUrl(url)
|
||||
if (!parsed) throw new Error("Invalid PR URL. Expected: https://github.com/owner/repo/pull/123")
|
||||
|
||||
@@ -462,7 +504,7 @@ export class WorktreeManager {
|
||||
await this.git.raw(["branch", branch, `${forkOwner}/${info.headRefName}`])
|
||||
}
|
||||
|
||||
return this.createWorktree({ existingBranch: branch })
|
||||
return this.createWorktreeImpl({ existingBranch: branch })
|
||||
}
|
||||
|
||||
private async fetchPRInfo(parsed: { owner: string; repo: string; number: number }): Promise<PRInfo> {
|
||||
|
||||
+1
-6
@@ -83,12 +83,7 @@ export class FimPromptBuilder {
|
||||
response += text
|
||||
}
|
||||
logtime("prep fim")
|
||||
const usageInfo = await model.generateFimResponse(
|
||||
formattedPrefix,
|
||||
prunedSuffix,
|
||||
onChunk,
|
||||
signal,
|
||||
)
|
||||
const usageInfo = await model.generateFimResponse(formattedPrefix, prunedSuffix, onChunk, signal)
|
||||
logtime("fim network")
|
||||
console.log("[FIM] response:", response)
|
||||
|
||||
|
||||
@@ -27,201 +27,199 @@ export type SSEStateHandler = (state: "connecting" | "connected" | "disconnected
|
||||
* straightforward improvement.
|
||||
*/
|
||||
export class SdkSSEAdapter {
|
||||
private readonly handlers = new Set<SSEEventHandler>()
|
||||
private readonly errorHandlers = new Set<SSEErrorHandler>()
|
||||
private readonly stateHandlers = new Set<SSEStateHandler>()
|
||||
private readonly handlers = new Set<SSEEventHandler>()
|
||||
private readonly errorHandlers = new Set<SSEErrorHandler>()
|
||||
private readonly stateHandlers = new Set<SSEStateHandler>()
|
||||
|
||||
private abortController: AbortController | null = null
|
||||
private heartbeatTimer: ReturnType<typeof setTimeout> | null = null
|
||||
private abortController: AbortController | null = null
|
||||
private heartbeatTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
private static readonly HEARTBEAT_TIMEOUT_MS = 90_000
|
||||
private static readonly RECONNECT_DELAY_MS = 250
|
||||
private static readonly HEARTBEAT_TIMEOUT_MS = 90_000
|
||||
private static readonly RECONNECT_DELAY_MS = 250
|
||||
|
||||
constructor(private readonly client: KiloClient) {}
|
||||
constructor(private readonly client: KiloClient) {}
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Start consuming the global SSE stream.
|
||||
* Calling `connect()` while already connected is a no-op.
|
||||
*/
|
||||
connect(): void {
|
||||
if (this.abortController) {
|
||||
console.log("[Kilo New] SSE: ⚠️ Already connected, skipping")
|
||||
return
|
||||
}
|
||||
/**
|
||||
* Start consuming the global SSE stream.
|
||||
* Calling `connect()` while already connected is a no-op.
|
||||
*/
|
||||
connect(): void {
|
||||
if (this.abortController) {
|
||||
console.log("[Kilo New] SSE: ⚠️ Already connected, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("[Kilo New] SSE: 🔌 connect() called")
|
||||
this.abortController = new AbortController()
|
||||
console.log('[Kilo New] SSE: 🔄 Setting state to "connecting"')
|
||||
this.notifyState("connecting")
|
||||
void this.consumeLoop(this.abortController.signal).catch((err) => {
|
||||
console.error("[Kilo New] SSE: Unhandled error in consumeLoop:", err)
|
||||
this.notifyError(err instanceof Error ? err : new Error(String(err)))
|
||||
})
|
||||
}
|
||||
console.log("[Kilo New] SSE: 🔌 connect() called")
|
||||
this.abortController = new AbortController()
|
||||
console.log('[Kilo New] SSE: 🔄 Setting state to "connecting"')
|
||||
this.notifyState("connecting")
|
||||
void this.consumeLoop(this.abortController.signal).catch((err) => {
|
||||
console.error("[Kilo New] SSE: Unhandled error in consumeLoop:", err)
|
||||
this.notifyError(err instanceof Error ? err : new Error(String(err)))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop consuming the SSE stream and abort any in-flight request.
|
||||
*/
|
||||
disconnect(): void {
|
||||
console.log("[Kilo New] SSE: 🔌 disconnect() called")
|
||||
this.abortController?.abort()
|
||||
this.abortController = null
|
||||
this.clearHeartbeat()
|
||||
}
|
||||
/**
|
||||
* Stop consuming the SSE stream and abort any in-flight request.
|
||||
*/
|
||||
disconnect(): void {
|
||||
console.log("[Kilo New] SSE: 🔌 disconnect() called")
|
||||
this.abortController?.abort()
|
||||
this.abortController = null
|
||||
this.clearHeartbeat()
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect and clear all registered handlers.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.disconnect()
|
||||
this.handlers.clear()
|
||||
this.errorHandlers.clear()
|
||||
this.stateHandlers.clear()
|
||||
}
|
||||
/**
|
||||
* Disconnect and clear all registered handlers.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.disconnect()
|
||||
this.handlers.clear()
|
||||
this.errorHandlers.clear()
|
||||
this.stateHandlers.clear()
|
||||
}
|
||||
|
||||
// ── Pub/sub ────────────────────────────────────────────────────────
|
||||
// ── Pub/sub ────────────────────────────────────────────────────────
|
||||
|
||||
onEvent(handler: SSEEventHandler): () => void {
|
||||
this.handlers.add(handler)
|
||||
return () => {
|
||||
this.handlers.delete(handler)
|
||||
}
|
||||
}
|
||||
onEvent(handler: SSEEventHandler): () => void {
|
||||
this.handlers.add(handler)
|
||||
return () => {
|
||||
this.handlers.delete(handler)
|
||||
}
|
||||
}
|
||||
|
||||
onError(handler: SSEErrorHandler): () => void {
|
||||
this.errorHandlers.add(handler)
|
||||
return () => {
|
||||
this.errorHandlers.delete(handler)
|
||||
}
|
||||
}
|
||||
onError(handler: SSEErrorHandler): () => void {
|
||||
this.errorHandlers.add(handler)
|
||||
return () => {
|
||||
this.errorHandlers.delete(handler)
|
||||
}
|
||||
}
|
||||
|
||||
onStateChange(handler: SSEStateHandler): () => void {
|
||||
this.stateHandlers.add(handler)
|
||||
return () => {
|
||||
this.stateHandlers.delete(handler)
|
||||
}
|
||||
}
|
||||
onStateChange(handler: SSEStateHandler): () => void {
|
||||
this.stateHandlers.add(handler)
|
||||
return () => {
|
||||
this.stateHandlers.delete(handler)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Internal ───────────────────────────────────────────────────────
|
||||
// ── Internal ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Main reconnection loop — mirrors the pattern in `global-sdk.tsx`.
|
||||
*/
|
||||
private async consumeLoop(signal: AbortSignal): Promise<void> {
|
||||
while (!signal.aborted) {
|
||||
const attempt = new AbortController()
|
||||
/**
|
||||
* Main reconnection loop — mirrors the pattern in `global-sdk.tsx`.
|
||||
*/
|
||||
private async consumeLoop(signal: AbortSignal): Promise<void> {
|
||||
while (!signal.aborted) {
|
||||
const attempt = new AbortController()
|
||||
|
||||
// Forward the outer abort to the per-attempt controller so
|
||||
// `disconnect()` cancels the current fetch immediately.
|
||||
const onAbort = () => attempt.abort()
|
||||
signal.addEventListener("abort", onAbort)
|
||||
// Forward the outer abort to the per-attempt controller so
|
||||
// `disconnect()` cancels the current fetch immediately.
|
||||
const onAbort = () => attempt.abort()
|
||||
signal.addEventListener("abort", onAbort)
|
||||
|
||||
try {
|
||||
console.log("[Kilo New] SSE: 🎬 Calling SDK global.event()...")
|
||||
const events = await this.client.global.event({
|
||||
signal: attempt.signal,
|
||||
onSseError: (error) => {
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
console.error("[Kilo New] SSE: ❌ SDK SSE error callback:", error)
|
||||
this.notifyError(error instanceof Error ? error : new Error(String(error)))
|
||||
},
|
||||
})
|
||||
try {
|
||||
console.log("[Kilo New] SSE: 🎬 Calling SDK global.event()...")
|
||||
const events = await this.client.global.event({
|
||||
signal: attempt.signal,
|
||||
onSseError: (error) => {
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
console.error("[Kilo New] SSE: ❌ SDK SSE error callback:", error)
|
||||
this.notifyError(error instanceof Error ? error : new Error(String(error)))
|
||||
},
|
||||
})
|
||||
|
||||
console.log("[Kilo New] SSE: ✅ Stream opened successfully")
|
||||
this.notifyState("connected")
|
||||
this.resetHeartbeat(attempt)
|
||||
console.log("[Kilo New] SSE: ✅ Stream opened successfully")
|
||||
this.notifyState("connected")
|
||||
this.resetHeartbeat(attempt)
|
||||
|
||||
for await (const event of events.stream) {
|
||||
if (signal.aborted) {
|
||||
break
|
||||
}
|
||||
for await (const event of events.stream) {
|
||||
if (signal.aborted) {
|
||||
break
|
||||
}
|
||||
|
||||
this.resetHeartbeat(attempt)
|
||||
this.resetHeartbeat(attempt)
|
||||
|
||||
// The SDK yields GlobalEvent = { directory, payload: Event }.
|
||||
const globalEvent = event as GlobalEvent
|
||||
console.log("[Kilo New] SSE: 📨 Event:", globalEvent.payload.type)
|
||||
this.notifyEvent(globalEvent.payload)
|
||||
}
|
||||
// The SDK yields GlobalEvent = { directory, payload: Event }.
|
||||
const globalEvent = event as GlobalEvent
|
||||
console.log("[Kilo New] SSE: 📨 Event:", globalEvent.payload.type)
|
||||
this.notifyEvent(globalEvent.payload)
|
||||
}
|
||||
|
||||
console.log("[Kilo New] SSE: 📭 Stream ended normally")
|
||||
} catch (error) {
|
||||
if (!signal.aborted) {
|
||||
console.error("[Kilo New] SSE: ❌ Stream error:", error)
|
||||
this.notifyError(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
} finally {
|
||||
signal.removeEventListener("abort", onAbort)
|
||||
this.clearHeartbeat()
|
||||
}
|
||||
console.log("[Kilo New] SSE: 📭 Stream ended normally")
|
||||
} catch (error) {
|
||||
if (!signal.aborted) {
|
||||
console.error("[Kilo New] SSE: ❌ Stream error:", error)
|
||||
this.notifyError(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
} finally {
|
||||
signal.removeEventListener("abort", onAbort)
|
||||
this.clearHeartbeat()
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
break
|
||||
}
|
||||
if (signal.aborted) {
|
||||
break
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Kilo New] SSE: 🔄 Reconnecting in ${SdkSSEAdapter.RECONNECT_DELAY_MS}ms...`,
|
||||
)
|
||||
this.notifyState("connecting")
|
||||
await new Promise((resolve) => setTimeout(resolve, SdkSSEAdapter.RECONNECT_DELAY_MS))
|
||||
}
|
||||
console.log(`[Kilo New] SSE: 🔄 Reconnecting in ${SdkSSEAdapter.RECONNECT_DELAY_MS}ms...`)
|
||||
this.notifyState("connecting")
|
||||
await new Promise((resolve) => setTimeout(resolve, SdkSSEAdapter.RECONNECT_DELAY_MS))
|
||||
}
|
||||
|
||||
this.notifyState("disconnected")
|
||||
}
|
||||
this.notifyState("disconnected")
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the heartbeat timer. If no event arrives within the timeout
|
||||
* window the per-attempt controller is aborted, causing the
|
||||
* `for await` loop to exit and the outer loop to reconnect.
|
||||
*/
|
||||
private resetHeartbeat(attempt: AbortController): void {
|
||||
this.clearHeartbeat()
|
||||
this.heartbeatTimer = setTimeout(() => {
|
||||
console.log("[Kilo New] SSE: ⏰ Heartbeat timeout — aborting stale connection")
|
||||
attempt.abort()
|
||||
}, SdkSSEAdapter.HEARTBEAT_TIMEOUT_MS)
|
||||
}
|
||||
/**
|
||||
* Reset the heartbeat timer. If no event arrives within the timeout
|
||||
* window the per-attempt controller is aborted, causing the
|
||||
* `for await` loop to exit and the outer loop to reconnect.
|
||||
*/
|
||||
private resetHeartbeat(attempt: AbortController): void {
|
||||
this.clearHeartbeat()
|
||||
this.heartbeatTimer = setTimeout(() => {
|
||||
console.log("[Kilo New] SSE: ⏰ Heartbeat timeout — aborting stale connection")
|
||||
attempt.abort()
|
||||
}, SdkSSEAdapter.HEARTBEAT_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
private clearHeartbeat(): void {
|
||||
if (this.heartbeatTimer) {
|
||||
clearTimeout(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
}
|
||||
private clearHeartbeat(): void {
|
||||
if (this.heartbeatTimer) {
|
||||
clearTimeout(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// ── Notify helpers ─────────────────────────────────────────────────
|
||||
// ── Notify helpers ─────────────────────────────────────────────────
|
||||
|
||||
private notifyEvent(event: Event): void {
|
||||
for (const handler of this.handlers) {
|
||||
try {
|
||||
handler(event)
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] SSE: Error in event handler:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
private notifyEvent(event: Event): void {
|
||||
for (const handler of this.handlers) {
|
||||
try {
|
||||
handler(event)
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] SSE: Error in event handler:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private notifyError(error: Error): void {
|
||||
for (const handler of this.errorHandlers) {
|
||||
try {
|
||||
handler(error)
|
||||
} catch (err) {
|
||||
console.error("[Kilo New] SSE: Error in error handler:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
private notifyError(error: Error): void {
|
||||
for (const handler of this.errorHandlers) {
|
||||
try {
|
||||
handler(error)
|
||||
} catch (err) {
|
||||
console.error("[Kilo New] SSE: Error in error handler:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private notifyState(state: "connecting" | "connected" | "disconnected"): void {
|
||||
for (const handler of this.stateHandlers) {
|
||||
try {
|
||||
handler(state)
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] SSE: Error in state handler:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
private notifyState(state: "connecting" | "connected" | "disconnected"): void {
|
||||
for (const handler of this.stateHandlers) {
|
||||
try {
|
||||
handler(state)
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] SSE: Error in state handler:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
{ "type": "module" }
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,11 @@ const mockVscode = {
|
||||
extensions: {
|
||||
getExtension: (id: string) => {
|
||||
if (id === "vscode.git") return undefined
|
||||
return { packageJSON: { version: "test", contributes: { configuration: { properties: {} } } }, isActive: true, exports: undefined }
|
||||
return {
|
||||
packageJSON: { version: "test", contributes: { configuration: { properties: {} } } },
|
||||
isActive: true,
|
||||
exports: undefined,
|
||||
}
|
||||
},
|
||||
},
|
||||
env: {
|
||||
@@ -63,7 +67,10 @@ const mockVscode = {
|
||||
CodeAction: class {
|
||||
command?: { command: string; title: string }
|
||||
isPreferred?: boolean
|
||||
constructor(public title: string, public kind: { value: string }) {}
|
||||
constructor(
|
||||
public title: string,
|
||||
public kind: { value: string },
|
||||
) {}
|
||||
},
|
||||
CodeActionKind: {
|
||||
QuickFix: kind("quickfix"),
|
||||
@@ -78,10 +85,16 @@ const mockVscode = {
|
||||
constructor(public uri: { scheme: string; fsPath: string }) {}
|
||||
},
|
||||
Position: class {
|
||||
constructor(public line: number, public character: number) {}
|
||||
constructor(
|
||||
public line: number,
|
||||
public character: number,
|
||||
) {}
|
||||
},
|
||||
Range: class {
|
||||
constructor(public start: { line: number; character: number }, public end: { line: number; character: number }) {}
|
||||
constructor(
|
||||
public start: { line: number; character: number },
|
||||
public end: { line: number; character: number },
|
||||
) {}
|
||||
},
|
||||
Disposable: class {
|
||||
constructor(private callback: () => void = noop) {}
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d385bd579ac5587b1f70416196f77b6465d6a69fedf7270b889eedb29d0ac73d
|
||||
size 14864
|
||||
oid sha256:a3774fc422d473693e38fafba61c00632fd49d2a0c61d2332b9c31482af78a2b
|
||||
size 15268
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5d69f44620c8e3a2f8d54ec97903c95cde04bef4655fc3b280a318e6da3cb0b5
|
||||
size 9082
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:db0467fd219dfc19b4b27e32ff29c7f81e68091c58bb6b9209e6baa4e5f396ac
|
||||
size 19507
|
||||
oid sha256:b8b1ba56d6632bfcbd0e4c5b0c1514cd80832339dde0c86568572ada656ca33d
|
||||
size 24134
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:04d857626cd157bf7f3325f50f48f5c9f95302a836f873cd9249aabbb6b22c8e
|
||||
size 25422
|
||||
oid sha256:278855516fe11d95b9de942d9690531c114b0a0b9ac0239c2ef31f82b420bd7e
|
||||
size 32467
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee4898a628110edf572fa27433bbb905d260225be0dd2154ccdfd4e4a2ea4c1e
|
||||
size 4415
|
||||
oid sha256:bcafa00cb615abbf253220f3b1c3163056bd18e1f5bf9632bc7229738936eff6
|
||||
size 4311
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:22e44ea6da3326afe29e5eba35c3df0602ee84506f91d4d449c8cf770cd741b4
|
||||
size 28468
|
||||
oid sha256:f6193ba79b4d5dda29bf39d1127dacd7e332755d1f6f42dd67ff667088975659
|
||||
size 35678
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cd8a53356f0aa0df5c07246296dc20273f5380a012cd579885b1974e3aeb99e1
|
||||
size 8496
|
||||
oid sha256:8f7fd08433524c113a2d7b3fed8bef5c023d9a451621f658b836c7967ca954aa
|
||||
size 8310
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:36abb08cb4cc89b797d49ce2639c6f6e1086d83a6d373bf7821c1035253018b7
|
||||
size 11181
|
||||
oid sha256:2a225a66c7981745089a46f20841d5f214d23d905b8b31194cfba9f278a3763d
|
||||
size 10965
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:607707e2201d1e50369a5897266136387b2444570b61ac2062c658335a0cdcc7
|
||||
size 5055
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:64ff66248b1c07209757eb6e27ccac4967650b144317ee21076ba82c129bc17b
|
||||
size 8846
|
||||
oid sha256:d2c75a7bb06035f07f364c7fb7a1eb0b813663658d018e4eea881c48cc0e276c
|
||||
size 8698
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:56517eed0845a01559a613124e5cf8f48945bbfc6f0fb6e3274ca5607954cb0c
|
||||
size 1293
|
||||
oid sha256:e0f7858b22d02fc7f30de4faae3f1f95ad7022e5e86741304b67d0f28f423e71
|
||||
size 1296
|
||||
|
||||
@@ -329,12 +329,12 @@ const AgentManagerContent: Component = () => {
|
||||
const [diffLoading, setDiffLoading] = createSignal(false)
|
||||
const [diffWidth, setDiffWidth] = createSignal(Math.round(window.innerWidth * 0.5))
|
||||
|
||||
// Full-screen review state (in-memory, per worktree)
|
||||
const [reviewOpenByWorktree, setReviewOpenByWorktree] = createSignal<Record<string, boolean>>({})
|
||||
const [reviewCommentsByWorktree, setReviewCommentsByWorktree] = createSignal<Record<string, ReviewComment[]>>({})
|
||||
// Full-screen review state (in-memory, per sidebar context: local/worktree)
|
||||
const [reviewOpenByContext, setReviewOpenByContext] = createSignal<Record<string, boolean>>({})
|
||||
const [reviewCommentsByContext, setReviewCommentsByContext] = createSignal<Record<string, ReviewComment[]>>({})
|
||||
const [reviewActive, setReviewActive] = createSignal(false)
|
||||
const [reviewDiffStyle, setReviewDiffStyle] = createSignal<"unified" | "split">("unified")
|
||||
// reviewOpen (memo below) controls tab presence for selected worktree.
|
||||
// reviewOpen (memo below) controls tab presence for selected context.
|
||||
|
||||
// Per-worktree git stats (diff additions/deletions, commits missing from origin)
|
||||
const [worktreeStats, setWorktreeStats] = createSignal<Record<string, WorktreeGitStats>>({})
|
||||
@@ -358,33 +358,33 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
const reviewOpen = createMemo(() => {
|
||||
const sel = selection()
|
||||
if (!sel || sel === LOCAL) return false
|
||||
return reviewOpenByWorktree()[sel] === true
|
||||
if (sel === null) return false
|
||||
return reviewOpenByContext()[sel] === true
|
||||
})
|
||||
|
||||
const setReviewOpenForWorktree = (worktreeId: string, open: boolean) => {
|
||||
setReviewOpenByWorktree((prev) => {
|
||||
if (prev[worktreeId] === open) return prev
|
||||
return { ...prev, [worktreeId]: open }
|
||||
const setReviewOpenForContext = (context: string, open: boolean) => {
|
||||
setReviewOpenByContext((prev) => {
|
||||
if (prev[context] === open) return prev
|
||||
return { ...prev, [context]: open }
|
||||
})
|
||||
}
|
||||
|
||||
const setReviewOpenForSelection = (open: boolean) => {
|
||||
const sel = selection()
|
||||
if (!sel || sel === LOCAL) return
|
||||
setReviewOpenForWorktree(sel, open)
|
||||
if (sel === null) return
|
||||
setReviewOpenForContext(sel, open)
|
||||
}
|
||||
|
||||
const reviewComments = createMemo(() => {
|
||||
const sel = selection()
|
||||
if (!sel || sel === LOCAL) return [] as ReviewComment[]
|
||||
return reviewCommentsByWorktree()[sel] ?? []
|
||||
if (sel === null) return [] as ReviewComment[]
|
||||
return reviewCommentsByContext()[sel] ?? []
|
||||
})
|
||||
|
||||
const setReviewCommentsForSelection = (comments: ReviewComment[]) => {
|
||||
const sel = selection()
|
||||
if (!sel || sel === LOCAL) return
|
||||
setReviewCommentsByWorktree((prev) => ({ ...prev, [sel]: comments }))
|
||||
if (sel === null) return
|
||||
setReviewCommentsByContext((prev) => ({ ...prev, [sel]: comments }))
|
||||
}
|
||||
|
||||
const applyStateForSelection = createMemo(() => {
|
||||
@@ -633,14 +633,14 @@ const AgentManagerContent: Component = () => {
|
||||
createEffect(() => {
|
||||
const ids = new Set(worktrees().map((wt) => wt.id))
|
||||
|
||||
setReviewOpenByWorktree((prev) => {
|
||||
const next = Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id)))
|
||||
setReviewOpenByContext((prev) => {
|
||||
const next = Object.fromEntries(Object.entries(prev).filter(([id]) => id === LOCAL || ids.has(id)))
|
||||
if (Object.keys(next).length === Object.keys(prev).length) return prev
|
||||
return next
|
||||
})
|
||||
|
||||
setReviewCommentsByWorktree((prev) => {
|
||||
const next = Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id)))
|
||||
setReviewCommentsByContext((prev) => {
|
||||
const next = Object.fromEntries(Object.entries(prev).filter(([id]) => id === LOCAL || ids.has(id)))
|
||||
if (Object.keys(next).length === Object.keys(prev).length) return prev
|
||||
return next
|
||||
})
|
||||
@@ -722,7 +722,7 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
createEffect(() => {
|
||||
const sel = selection()
|
||||
if (!sel || sel === LOCAL) {
|
||||
if (sel === null) {
|
||||
if (reviewActive()) setReviewActive(false)
|
||||
return
|
||||
}
|
||||
@@ -903,6 +903,7 @@ const AgentManagerContent: Component = () => {
|
||||
session.clearCurrentSession()
|
||||
vscode.postMessage({ type: "agentManager.showExistingLocalTerminal" })
|
||||
}
|
||||
setReviewActive(remembered === REVIEW_TAB_ID && reviewOpenByContext()[LOCAL] === true)
|
||||
}
|
||||
|
||||
const selectWorktree = (worktreeId: string) => {
|
||||
@@ -919,7 +920,7 @@ const AgentManagerContent: Component = () => {
|
||||
} else {
|
||||
session.setCurrentSessionID(undefined)
|
||||
}
|
||||
setReviewActive(remembered === REVIEW_TAB_ID && reviewOpenByWorktree()[worktreeId] === true)
|
||||
setReviewActive(remembered === REVIEW_TAB_ID && reviewOpenByContext()[worktreeId] === true)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -1288,9 +1289,13 @@ const AgentManagerContent: Component = () => {
|
||||
return
|
||||
}
|
||||
if (review) {
|
||||
// Review tab is open but no specific session — try using any session in the worktree
|
||||
const sel = selection()
|
||||
if (sel && sel !== LOCAL) {
|
||||
// Review tab is open but no specific session — use local sentinel for local,
|
||||
// or any session in the selected worktree.
|
||||
if (sel === LOCAL) {
|
||||
vscode.postMessage({ type: "agentManager.startDiffWatch", sessionId: LOCAL })
|
||||
return
|
||||
}
|
||||
if (sel) {
|
||||
const managed = managedSessions().find((ms) => ms.worktreeId === sel)
|
||||
if (managed) {
|
||||
vscode.postMessage({ type: "agentManager.startDiffWatch", sessionId: managed.id })
|
||||
@@ -1311,9 +1316,9 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
const openReviewTab = () => {
|
||||
const sel = selection()
|
||||
if (!sel || sel === LOCAL) return
|
||||
if (sel === null) return
|
||||
setDiffOpen(false)
|
||||
setReviewOpenForWorktree(sel, true)
|
||||
setReviewOpenForContext(sel, true)
|
||||
setReviewActive(true)
|
||||
}
|
||||
|
||||
@@ -1333,16 +1338,18 @@ const AgentManagerContent: Component = () => {
|
||||
setReviewOpenForSelection(false)
|
||||
}
|
||||
|
||||
// Data for the review tab: use current session's diff data, or first available for the worktree
|
||||
// Data for the review tab: use local diff data for local context,
|
||||
// current session for selected worktree context, or first available in that worktree.
|
||||
const reviewDiffs = createMemo(() => {
|
||||
const data = diffDatas()
|
||||
const sel = selection()
|
||||
const id = session.currentSessionID()
|
||||
if (sel === LOCAL) return data[LOCAL] ?? []
|
||||
if (id && data[id]) {
|
||||
const current = managedSessions().find((s) => s.id === id)
|
||||
if (sel && sel !== LOCAL && current?.worktreeId === sel) return data[id]!
|
||||
if (sel && current?.worktreeId === sel) return data[id]!
|
||||
}
|
||||
if (!sel || sel === LOCAL) return []
|
||||
if (!sel) return []
|
||||
const ids = managedSessions()
|
||||
.filter((s) => s.worktreeId === sel)
|
||||
.map((s) => s.id)
|
||||
@@ -1569,8 +1576,9 @@ const AgentManagerContent: Component = () => {
|
||||
const tabIds = createMemo(() => {
|
||||
const ids = activeTabs().map((s) => s.id)
|
||||
const sel = selection()
|
||||
if (!sel || sel === LOCAL) return ids
|
||||
if (sel === null) return ids
|
||||
const current = reviewOpen() ? [...ids, REVIEW_TAB_ID] : ids
|
||||
if (sel === LOCAL) return current
|
||||
return applyTabOrder(
|
||||
current.map((id) => ({ id })),
|
||||
worktreeTabOrder()[sel],
|
||||
@@ -1588,7 +1596,12 @@ const AgentManagerContent: Component = () => {
|
||||
if (typeof from !== "string" || typeof to !== "string") return
|
||||
const sel = selection()
|
||||
if (sel === LOCAL) {
|
||||
setLocalSessionIDs((prev) => reorderTabs(prev, from, to) ?? prev)
|
||||
setLocalSessionIDs((prev) => {
|
||||
const ids = reviewOpen() ? [...prev, REVIEW_TAB_ID] : prev
|
||||
const reordered = reorderTabs(ids, from, to)
|
||||
if (!reordered) return prev
|
||||
return reordered.filter((id) => id !== REVIEW_TAB_ID)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (sel) {
|
||||
@@ -2037,7 +2050,7 @@ const AgentManagerContent: Component = () => {
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
icon="close-small"
|
||||
icon="trash"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={t("agentManager.worktree.delete")}
|
||||
@@ -2150,6 +2163,11 @@ const AgentManagerContent: Component = () => {
|
||||
</Show>
|
||||
)
|
||||
})()}
|
||||
<div class="am-hover-card-divider" />
|
||||
<div class="am-hover-card-hint">
|
||||
<Icon name="edit" size="small" />
|
||||
<span>{t("agentManager.worktree.doubleClickRename")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCard>
|
||||
</>
|
||||
@@ -2418,7 +2436,7 @@ const AgentManagerContent: Component = () => {
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
<Show when={selection() !== LOCAL}>
|
||||
<Show when={selection() !== null}>
|
||||
<Tooltip value={t("command.review.toggle")} placement="bottom">
|
||||
<IconButton
|
||||
icon="expand"
|
||||
@@ -2568,7 +2586,7 @@ const AgentManagerContent: Component = () => {
|
||||
comments={reviewComments()}
|
||||
onCommentsChange={setReviewCommentsForSelection}
|
||||
onClose={() => setDiffOpen(false)}
|
||||
onExpand={selection() !== LOCAL ? openReviewTab : undefined}
|
||||
onExpand={selection() !== null ? openReviewTab : undefined}
|
||||
onOpenFile={(file) => {
|
||||
const id = session.currentSessionID()
|
||||
if (id) vscode.postMessage({ type: "agentManager.openFile", sessionId: id, filePath: file })
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
color: var(--text-base);
|
||||
font-family: inherit;
|
||||
font-size: var(--font-size-base);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -50,15 +51,17 @@
|
||||
}
|
||||
|
||||
.am-local-item-active {
|
||||
background: var(--surface-base-active);
|
||||
background: var(--surface-interactive-base);
|
||||
color: var(--text-on-interactive-base);
|
||||
}
|
||||
|
||||
.am-local-item-active .am-local-icon {
|
||||
color: var(--text-base);
|
||||
color: var(--text-on-interactive-base);
|
||||
}
|
||||
|
||||
.am-local-item-active .am-local-branch {
|
||||
color: var(--text-weak);
|
||||
color: var(--text-on-interactive-base);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.am-local-item-active .am-stat-files {
|
||||
@@ -469,6 +472,7 @@ button.am-section-toggle:hover .am-section-label {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
color: var(--text-base);
|
||||
font-family: inherit;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
@@ -481,8 +485,8 @@ button.am-section-toggle:hover .am-section-label {
|
||||
}
|
||||
|
||||
.am-item-active {
|
||||
background: var(--surface-base-active);
|
||||
color: var(--text-base);
|
||||
background: var(--surface-interactive-base);
|
||||
color: var(--text-on-interactive-base);
|
||||
}
|
||||
|
||||
.am-item-title {
|
||||
@@ -502,7 +506,8 @@ button.am-section-toggle:hover .am-section-label {
|
||||
}
|
||||
|
||||
.am-item-active .am-item-time {
|
||||
color: var(--text-weaker);
|
||||
color: var(--text-on-interactive-base);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Promote button on session rows */
|
||||
@@ -2042,6 +2047,18 @@ button.am-section-toggle:hover .am-section-label {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.am-hover-card-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--text-weaker);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.am-hover-card-hint [data-component="icon"] {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Keyboard shortcuts dialog */
|
||||
|
||||
.am-shortcuts {
|
||||
|
||||
@@ -131,10 +131,10 @@ export const dict = {
|
||||
"agentManager.review.sendToChat": "Pošalji u chat",
|
||||
"agentManager.review.collapsedOnly": "{{count}} sažeto",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}} sažeto, {{large}} velikih",
|
||||
"agentManager.review.largeFileCollapsed": "Velika datoteka (skupljeno)",
|
||||
"agentManager.review.endOfLongDiff": "Stigli ste do kraja!",
|
||||
"agentManager.review.largeFileCollapsed": "Velika datoteka (sažeto)",
|
||||
"agentManager.review.endOfLongDiff": "Došli ste do kraja!",
|
||||
|
||||
"agentManager.import.pullRequest": "Zahtjev za povlačenje",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "Zalijepite PR URL...",
|
||||
"agentManager.import.open": "Otvori",
|
||||
"agentManager.import.branches": "Branchevi",
|
||||
|
||||
@@ -131,10 +131,10 @@ export const dict = {
|
||||
"agentManager.review.sendToChat": "Send til chat",
|
||||
"agentManager.review.collapsedOnly": "{{count}} foldet sammen",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}} foldet sammen, {{large}} store",
|
||||
"agentManager.review.largeFileCollapsed": "Stor fil (kollapset)",
|
||||
"agentManager.review.endOfLongDiff": "Du er nået til slutningen!",
|
||||
"agentManager.review.largeFileCollapsed": "Stor fil (sammenklappet)",
|
||||
"agentManager.review.endOfLongDiff": "Du nåede slutningen!",
|
||||
|
||||
"agentManager.import.pullRequest": "Fletteanmodning",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "Indsæt PR URL...",
|
||||
"agentManager.import.open": "Åbn",
|
||||
"agentManager.import.branches": "Branches",
|
||||
|
||||
@@ -132,10 +132,10 @@ export const dict = {
|
||||
"agentManager.review.sendToChat": "An Chat senden",
|
||||
"agentManager.review.collapsedOnly": "{{count}} eingeklappt",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}} eingeklappt, {{large}} groß",
|
||||
"agentManager.review.largeFileCollapsed": "Große Datei (zugeklappt)",
|
||||
"agentManager.review.largeFileCollapsed": "Große Datei (eingeklappt)",
|
||||
"agentManager.review.endOfLongDiff": "Du hast das Ende erreicht!",
|
||||
|
||||
"agentManager.import.pullRequest": "Zusammenführungsanfrage",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "PR-URL einfügen...",
|
||||
"agentManager.import.open": "Öffnen",
|
||||
"agentManager.import.branches": "Branches",
|
||||
|
||||
@@ -133,9 +133,9 @@ export const dict = {
|
||||
"agentManager.review.collapsedOnly": "{{count}} contraídos",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}} contraídos, {{large}} grandes",
|
||||
"agentManager.review.largeFileCollapsed": "Archivo grande (contraído)",
|
||||
"agentManager.review.endOfLongDiff": "¡Has llegado al final!",
|
||||
"agentManager.review.endOfLongDiff": "¡Llegaste al final!",
|
||||
|
||||
"agentManager.import.pullRequest": "Solicitud de extracción",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "Pegar URL del PR...",
|
||||
"agentManager.import.open": "Abrir",
|
||||
"agentManager.import.branches": "Branches",
|
||||
|
||||
@@ -133,10 +133,10 @@ export const dict = {
|
||||
"agentManager.review.sendToChat": "Envoyer au chat",
|
||||
"agentManager.review.collapsedOnly": "{{count}} repliés",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}} repliés, {{large}} volumineux",
|
||||
"agentManager.review.largeFileCollapsed": "Fichier volumineux (réduit)",
|
||||
"agentManager.review.largeFileCollapsed": "Fichier volumineux (replié)",
|
||||
"agentManager.review.endOfLongDiff": "Vous êtes arrivé à la fin !",
|
||||
|
||||
"agentManager.import.pullRequest": "Demande d'extraction",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "Coller l'URL du PR...",
|
||||
"agentManager.import.open": "Ouvrir",
|
||||
"agentManager.import.branches": "Branches",
|
||||
|
||||
@@ -133,10 +133,10 @@ export const dict = {
|
||||
"agentManager.review.sendToChat": "チャットに送信",
|
||||
"agentManager.review.collapsedOnly": "{{count}} 件折りたたみ",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}} 件折りたたみ、{{large}} 件がサイズ大",
|
||||
"agentManager.review.largeFileCollapsed": "大きなファイル (折りたたみ済み)",
|
||||
"agentManager.review.largeFileCollapsed": "大きなファイル(折りたたみ)",
|
||||
"agentManager.review.endOfLongDiff": "最後まで到達しました!",
|
||||
|
||||
"agentManager.import.pullRequest": "プルリクエスト",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "PR URLを貼り付け...",
|
||||
"agentManager.import.open": "開く",
|
||||
"agentManager.import.branches": "ブランチ",
|
||||
|
||||
@@ -132,10 +132,10 @@ export const dict = {
|
||||
"agentManager.review.sendToChat": "채팅으로 보내기",
|
||||
"agentManager.review.collapsedOnly": "{{count}}개 접힘",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}}개 접힘, {{large}}개 대용량",
|
||||
"agentManager.review.largeFileCollapsed": "큰 파일 (접힘)",
|
||||
"agentManager.review.endOfLongDiff": "끝까지 확인하셨습니다!",
|
||||
"agentManager.review.largeFileCollapsed": "큰 파일(접힘)",
|
||||
"agentManager.review.endOfLongDiff": "끝까지 도달했습니다!",
|
||||
|
||||
"agentManager.import.pullRequest": "풀 리퀘스트",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "PR URL 붙여넣기...",
|
||||
"agentManager.import.open": "열기",
|
||||
"agentManager.import.branches": "브랜치",
|
||||
|
||||
@@ -131,10 +131,10 @@ export const dict = {
|
||||
"agentManager.review.sendToChat": "Send til chat",
|
||||
"agentManager.review.collapsedOnly": "{{count}} kollapset",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}} kollapset, {{large}} store",
|
||||
"agentManager.review.largeFileCollapsed": "Stor fil (kollapset)",
|
||||
"agentManager.review.endOfLongDiff": "Du har nådd slutten!",
|
||||
"agentManager.review.largeFileCollapsed": "Stor fil (sammenfoldet)",
|
||||
"agentManager.review.endOfLongDiff": "Du nådde slutten!",
|
||||
|
||||
"agentManager.import.pullRequest": "Fletteforespørsel",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "Lim inn PR URL...",
|
||||
"agentManager.import.open": "Åpne",
|
||||
"agentManager.import.branches": "Branches",
|
||||
|
||||
@@ -132,10 +132,10 @@ export const dict = {
|
||||
"agentManager.review.sendToChat": "ส่งไปยังแชท",
|
||||
"agentManager.review.collapsedOnly": "ยุบ {{count}} รายการ",
|
||||
"agentManager.review.collapsedWithLarge": "ยุบ {{collapsed}} รายการ, ขนาดใหญ่ {{large}} รายการ",
|
||||
"agentManager.review.largeFileCollapsed": "ไฟล์ขนาดใหญ่ (ย่อไว้)",
|
||||
"agentManager.review.endOfLongDiff": "คุณมาถึงส่วนท้ายแล้ว!",
|
||||
"agentManager.review.largeFileCollapsed": "ไฟล์ขนาดใหญ่ (พับอยู่)",
|
||||
"agentManager.review.endOfLongDiff": "คุณมาถึงท้ายสุดแล้ว!",
|
||||
|
||||
"agentManager.import.pullRequest": "คำขอดึง",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "วาง URL ของ PR...",
|
||||
"agentManager.import.open": "เปิด",
|
||||
"agentManager.import.branches": "Branches",
|
||||
|
||||
@@ -131,9 +131,9 @@ export const dict = {
|
||||
"agentManager.review.collapsedOnly": "{{count}} 个已折叠",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}} 个已折叠,{{large}} 个过大",
|
||||
"agentManager.review.largeFileCollapsed": "大文件(已折叠)",
|
||||
"agentManager.review.endOfLongDiff": "您已到达末尾!",
|
||||
"agentManager.review.endOfLongDiff": "你已经到末尾了!",
|
||||
|
||||
"agentManager.import.pullRequest": "拉取请求",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "粘贴 PR URL...",
|
||||
"agentManager.import.open": "打开",
|
||||
"agentManager.import.branches": "分支",
|
||||
|
||||
@@ -130,10 +130,10 @@ export const dict = {
|
||||
"agentManager.review.sendToChat": "傳送到聊天",
|
||||
"agentManager.review.collapsedOnly": "{{count}} 個已摺疊",
|
||||
"agentManager.review.collapsedWithLarge": "{{collapsed}} 個已摺疊,{{large}} 個過大",
|
||||
"agentManager.review.largeFileCollapsed": "大檔案(已摺疊)",
|
||||
"agentManager.review.endOfLongDiff": "您已到達底部!",
|
||||
"agentManager.review.largeFileCollapsed": "大型檔案(已摺疊)",
|
||||
"agentManager.review.endOfLongDiff": "你已經到最後了!",
|
||||
|
||||
"agentManager.import.pullRequest": "拉取請求",
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "貼上 PR URL...",
|
||||
"agentManager.import.open": "開啟",
|
||||
"agentManager.import.branches": "分支",
|
||||
|
||||
@@ -170,7 +170,7 @@ const AppContent: Component = () => {
|
||||
const handleViewAction = (action: string) => {
|
||||
switch (action) {
|
||||
case "plusButtonClicked":
|
||||
session.clearCurrentSession()
|
||||
window.dispatchEvent(new CustomEvent("newTaskRequest"))
|
||||
setCurrentView("newTask")
|
||||
break
|
||||
case "marketplaceButtonClicked":
|
||||
|
||||
@@ -60,8 +60,7 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
|
||||
}
|
||||
|
||||
// Questions linked to this message (rendered after the last part)
|
||||
const questionForMessage = () =>
|
||||
questions().find((q) => q.tool!.messageID === props.message.id)
|
||||
const questionForMessage = () => questions().find((q) => q.tool!.messageID === props.message.id)
|
||||
|
||||
const [responding, setResponding] = createSignal(false)
|
||||
|
||||
@@ -79,7 +78,7 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
|
||||
const perm = () => permissionForPart(part)
|
||||
return (
|
||||
<Show when={PART_MAPPING[part.type]}>
|
||||
<div data-component="tool-part-wrapper" data-permission={!!perm()}>
|
||||
<div data-component="tool-part-wrapper" data-permission={!!perm()} data-part-type={part.type}>
|
||||
<Part
|
||||
part={part}
|
||||
message={props.message as SDKMessage}
|
||||
@@ -88,7 +87,7 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
|
||||
/>
|
||||
<Show when={perm()} keyed>
|
||||
{(p) => (
|
||||
<div data-component="permission-prompt">
|
||||
<div data-component="permission-prompt" onClick={(e: MouseEvent) => e.stopPropagation()}>
|
||||
<Show when={p.patterns.length > 0}>
|
||||
<div class="permission-dock-patterns">
|
||||
<For each={p.patterns}>
|
||||
@@ -97,13 +96,28 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
|
||||
</div>
|
||||
</Show>
|
||||
<div data-slot="permission-actions">
|
||||
<Button variant="ghost" size="small" onClick={() => decide(p.id, "reject")} disabled={responding()}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => decide(p.id, "reject")}
|
||||
disabled={responding()}
|
||||
>
|
||||
{language.t("ui.permission.deny")}
|
||||
</Button>
|
||||
<Button variant="secondary" size="small" onClick={() => decide(p.id, "always")} disabled={responding()}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => decide(p.id, "always")}
|
||||
disabled={responding()}
|
||||
>
|
||||
{language.t("ui.permission.allowAlways")}
|
||||
</Button>
|
||||
<Button variant="primary" size="small" onClick={() => decide(p.id, "once")} disabled={responding()}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={() => decide(p.id, "once")}
|
||||
disabled={responding()}
|
||||
>
|
||||
{language.t("ui.permission.allowOnce")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Main chat container that combines all chat components
|
||||
*/
|
||||
|
||||
import { Component, For, Show, createSignal, onCleanup, onMount } from "solid-js"
|
||||
import { Component, For, Show, createSignal, createEffect, on, onCleanup, onMount } from "solid-js"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { BasicTool } from "@kilocode/kilo-ui/basic-tool"
|
||||
import { TaskHeader } from "./TaskHeader"
|
||||
@@ -23,6 +23,8 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
const language = useLanguage()
|
||||
|
||||
const id = () => session.currentSessionID()
|
||||
const hasMessages = () => session.messages().length > 0
|
||||
const idle = () => session.status() !== "busy"
|
||||
const sessionQuestions = () => session.questions().filter((q) => q.sessionID === id())
|
||||
const sessionPermissions = () => session.permissions().filter((p) => p.sessionID === id())
|
||||
|
||||
@@ -30,6 +32,17 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
const permissionRequest = () => sessionPermissions().find((p) => !p.tool)
|
||||
const blocked = () => sessionPermissions().length > 0 || sessionQuestions().length > 0
|
||||
|
||||
// When a bottom-dock permission/question disappears while the session is busy,
|
||||
// the scroll container grows taller. Dispatch a custom event so MessageList can
|
||||
// resume auto-scroll.
|
||||
createEffect(
|
||||
on(blocked, (isBlocked, wasBlocked) => {
|
||||
if (wasBlocked && !isBlocked && !idle()) {
|
||||
window.dispatchEvent(new CustomEvent("resumeAutoScroll"))
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const [responding, setResponding] = createSignal(false)
|
||||
|
||||
onMount(() => {
|
||||
@@ -102,6 +115,19 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={hasMessages() && idle() && !blocked()}>
|
||||
<div class="new-task-button-wrapper">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
data-full-width="true"
|
||||
onClick={() => window.dispatchEvent(new CustomEvent("newTaskRequest"))}
|
||||
aria-label={language.t("command.session.new.task")}
|
||||
>
|
||||
{language.t("command.session.new.task")}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!blocked()}>
|
||||
<PromptInput />
|
||||
</Show>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* Shows recent sessions in the empty state for quick resumption.
|
||||
*/
|
||||
|
||||
import { Component, For, Show, createEffect, createMemo, JSX } from "solid-js"
|
||||
import { Component, For, Show, createEffect, createMemo, onCleanup, JSX } from "solid-js"
|
||||
import { Spinner } from "@kilocode/kilo-ui/spinner"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
|
||||
@@ -48,6 +48,11 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
overflowAnchor: "dynamic",
|
||||
})
|
||||
|
||||
// Resume auto-scroll when a bottom-dock permission/question is dismissed
|
||||
const onResumeAutoScroll = () => autoScroll.resume()
|
||||
window.addEventListener("resumeAutoScroll", onResumeAutoScroll)
|
||||
onCleanup(() => window.removeEventListener("resumeAutoScroll", onResumeAutoScroll))
|
||||
|
||||
let loaded = false
|
||||
createEffect(() => {
|
||||
if (!loaded && server.isConnected() && session.sessions().length === 0) {
|
||||
|
||||
@@ -72,6 +72,16 @@ export const PromptInput: Component = () => {
|
||||
window.addEventListener("focusPrompt", onFocusPrompt)
|
||||
onCleanup(() => window.removeEventListener("focusPrompt", onFocusPrompt))
|
||||
|
||||
// Start a new task, carrying over the current prompt text (without auto-sending it)
|
||||
const onNewTaskRequest = () => {
|
||||
const prompt = text().trim()
|
||||
// Pre-populate the draft for the new (empty) session so the effect restores it
|
||||
if (prompt) drafts.set("__new__", prompt)
|
||||
session.clearCurrentSession()
|
||||
}
|
||||
window.addEventListener("newTaskRequest", onNewTaskRequest)
|
||||
onCleanup(() => window.removeEventListener("newTaskRequest", onNewTaskRequest))
|
||||
|
||||
const isBusy = () => session.status() === "busy"
|
||||
const isDisabled = () => !server.isConnected()
|
||||
const canSend = () => (text().trim().length > 0 || imageAttach.images().length > 0) && !isBusy() && !isDisabled()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* QuestionDock component
|
||||
* Displays question requests from the AI assistant inline above the prompt input.
|
||||
* Uses kilo-ui's data-component/data-slot CSS pattern for styling.
|
||||
* Uses kilo-ui's DockPrompt component for proper surface styling.
|
||||
*/
|
||||
|
||||
import { Component, For, Show, createMemo, createEffect } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { DockPrompt } from "@kilocode/kilo-ui/dock-prompt"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useLanguage } from "../../context/language"
|
||||
@@ -148,10 +149,11 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-component="tool-part-wrapper" data-question="true">
|
||||
<div data-component="question-prompt">
|
||||
<div data-slot="question-body">
|
||||
<div data-slot="question-header">
|
||||
<div onClick={(e: MouseEvent) => e.stopPropagation()}>
|
||||
<DockPrompt
|
||||
kind="question"
|
||||
header={
|
||||
<>
|
||||
<div data-slot="question-header-title">{summary()}</div>
|
||||
<Show when={!single()}>
|
||||
<div data-slot="question-progress">
|
||||
@@ -177,62 +179,65 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={!confirm()}>
|
||||
<div data-slot="question-content">
|
||||
<div data-slot="question-text">{question()?.question}</div>
|
||||
<Show
|
||||
when={multi()}
|
||||
fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}
|
||||
>
|
||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" size="small" onClick={reject} disabled={store.sending}>
|
||||
{language.t("ui.common.dismiss")}
|
||||
</Button>
|
||||
<div data-slot="question-footer-actions">
|
||||
<Show when={store.tab > 0}>
|
||||
<Button variant="secondary" size="small" onClick={back} disabled={store.sending}>
|
||||
{language.t("ui.common.back")}
|
||||
</Button>
|
||||
</Show>
|
||||
<div data-slot="question-options">
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-picked={picked()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(i())}
|
||||
>
|
||||
<span data-slot="question-option-check" aria-hidden="true">
|
||||
<span
|
||||
data-slot="question-option-box"
|
||||
data-type={multi() ? "checkbox" : "radio"}
|
||||
data-picked={picked()}
|
||||
>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{opt.label}</span>
|
||||
<Show when={opt.description}>
|
||||
<span data-slot="option-description">{opt.description}</span>
|
||||
</Show>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<Show when={question()?.custom !== false}>
|
||||
<Show
|
||||
when={confirm()}
|
||||
fallback={
|
||||
<Button
|
||||
variant={last() && single() ? "primary" : "secondary"}
|
||||
size="small"
|
||||
onClick={last() && single() ? submit : () => selectTab(store.tab + 1)}
|
||||
disabled={store.sending || (!confirm() && (store.answers[store.tab]?.length ?? 0) === 0)}
|
||||
>
|
||||
{last() && single()
|
||||
? language.t("ui.common.submit")
|
||||
: last()
|
||||
? language.t("common.review")
|
||||
: language.t("ui.common.next")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Button variant="primary" size="small" onClick={submit} disabled={store.sending}>
|
||||
{language.t("ui.common.submit")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Show when={!confirm()}>
|
||||
<div data-slot="question-text">{question()?.question}</div>
|
||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||
</Show>
|
||||
<div data-slot="question-options">
|
||||
<For each={options()}>
|
||||
{(opt, i) => {
|
||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||
return (
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-custom="true"
|
||||
data-picked={customPicked()}
|
||||
data-picked={picked()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(options().length)}
|
||||
onClick={() => selectOption(i())}
|
||||
>
|
||||
<span data-slot="question-option-check" aria-hidden="true">
|
||||
<span
|
||||
data-slot="question-option-box"
|
||||
data-type={multi() ? "checkbox" : "radio"}
|
||||
data-picked={customPicked()}
|
||||
data-picked={picked()}
|
||||
>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
@@ -240,101 +245,94 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<span data-slot="option-description" data-placeholder={!input()}>
|
||||
{input() || language.t("ui.question.custom.placeholder")}
|
||||
</span>
|
||||
<span data-slot="option-label">{opt.label}</span>
|
||||
<Show when={opt.description}>
|
||||
<span data-slot="option-description">{opt.description}</span>
|
||||
</Show>
|
||||
</span>
|
||||
</button>
|
||||
<Show when={store.editing}>
|
||||
<form data-slot="custom-input-form" onSubmit={handleCustomSubmit}>
|
||||
<input
|
||||
ref={(el) => setTimeout(() => el.focus(), 0)}
|
||||
type="text"
|
||||
data-slot="custom-input"
|
||||
placeholder={language.t("ui.question.custom.placeholder")}
|
||||
value={input()}
|
||||
disabled={store.sending}
|
||||
onInput={(e) => {
|
||||
const inputs = [...store.custom]
|
||||
inputs[store.tab] = e.currentTarget.value
|
||||
setStore("custom", inputs)
|
||||
}}
|
||||
/>
|
||||
<Button type="submit" variant="primary" size="small" disabled={store.sending}>
|
||||
{multi() ? language.t("ui.common.add") : language.t("ui.common.submit")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
disabled={store.sending}
|
||||
onClick={() => setStore("editing", false)}
|
||||
>
|
||||
{language.t("ui.common.cancel")}
|
||||
</Button>
|
||||
</form>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={confirm()}>
|
||||
<div data-slot="question-review">
|
||||
<div data-slot="review-title">{language.t("ui.messagePart.review.title")}</div>
|
||||
<For each={questions()}>
|
||||
{(q, index) => {
|
||||
const value = () => store.answers[index()]?.join(", ") ?? ""
|
||||
const answered = () => Boolean(value())
|
||||
return (
|
||||
<div data-slot="review-item">
|
||||
<span data-slot="review-label">{q.question}</span>
|
||||
<span data-slot="review-value" data-answered={answered()}>
|
||||
{answered() ? value() : language.t("ui.question.review.notAnswered")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div data-slot="question-footer">
|
||||
<Button variant="ghost" size="small" onClick={reject} disabled={store.sending}>
|
||||
{language.t("ui.common.dismiss")}
|
||||
</Button>
|
||||
<div data-slot="question-footer-actions">
|
||||
<Show when={store.tab > 0}>
|
||||
<Button variant="secondary" size="small" onClick={back} disabled={store.sending}>
|
||||
{language.t("ui.common.back")}
|
||||
</Button>
|
||||
</Show>
|
||||
<Show
|
||||
when={confirm()}
|
||||
fallback={
|
||||
<Button
|
||||
variant={last() && single() ? "primary" : "secondary"}
|
||||
size="small"
|
||||
onClick={last() && single() ? submit : () => selectTab(store.tab + 1)}
|
||||
disabled={store.sending || (!confirm() && (store.answers[store.tab]?.length ?? 0) === 0)}
|
||||
>
|
||||
{last() && single()
|
||||
? language.t("ui.common.submit")
|
||||
: last()
|
||||
? language.t("common.review")
|
||||
: language.t("ui.common.next")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Button variant="primary" size="small" onClick={submit} disabled={store.sending}>
|
||||
{language.t("ui.common.submit")}
|
||||
</Button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<Show when={question()?.custom !== false}>
|
||||
<button
|
||||
data-slot="question-option"
|
||||
data-custom="true"
|
||||
data-picked={customPicked()}
|
||||
disabled={store.sending}
|
||||
onClick={() => selectOption(options().length)}
|
||||
>
|
||||
<span data-slot="question-option-check" aria-hidden="true">
|
||||
<span
|
||||
data-slot="question-option-box"
|
||||
data-type={multi() ? "checkbox" : "radio"}
|
||||
data-picked={customPicked()}
|
||||
>
|
||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||
<Icon name="check-small" size="small" />
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
<span data-slot="question-option-main">
|
||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||
<span data-slot="option-description" data-placeholder={!input()}>
|
||||
{input() || language.t("ui.question.custom.placeholder")}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<Show when={store.editing}>
|
||||
<form data-slot="custom-input-form" onSubmit={handleCustomSubmit}>
|
||||
<input
|
||||
ref={(el) => setTimeout(() => el.focus(), 0)}
|
||||
type="text"
|
||||
data-slot="custom-input"
|
||||
placeholder={language.t("ui.question.custom.placeholder")}
|
||||
value={input()}
|
||||
disabled={store.sending}
|
||||
onInput={(e) => {
|
||||
const inputs = [...store.custom]
|
||||
inputs[store.tab] = e.currentTarget.value
|
||||
setStore("custom", inputs)
|
||||
}}
|
||||
/>
|
||||
<Button type="submit" variant="primary" size="small" disabled={store.sending}>
|
||||
{multi() ? language.t("ui.common.add") : language.t("ui.common.submit")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
disabled={store.sending}
|
||||
onClick={() => setStore("editing", false)}
|
||||
>
|
||||
{language.t("ui.common.cancel")}
|
||||
</Button>
|
||||
</form>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={confirm()}>
|
||||
<div data-slot="question-review">
|
||||
<div data-slot="review-title">{language.t("ui.messagePart.review.title")}</div>
|
||||
<For each={questions()}>
|
||||
{(q, index) => {
|
||||
const value = () => store.answers[index()]?.join(", ") ?? ""
|
||||
const answered = () => Boolean(value())
|
||||
return (
|
||||
<div data-slot="review-item">
|
||||
<span data-slot="review-label">{q.question}</span>
|
||||
<span data-slot="review-value" data-answered={answered()}>
|
||||
{answered() ? value() : language.t("ui.question.review.notAnswered")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</DockPrompt>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "استخدام اللغة: {{language}}",
|
||||
|
||||
"command.session.new": "جلسة جديدة",
|
||||
"command.session.new.task": "مهمة جديدة",
|
||||
"command.file.open": "فتح ملف",
|
||||
"command.tab.close": "إغلاق علامة التبويب",
|
||||
"command.context.addSelection": "إضافة التحديد إلى السياق",
|
||||
@@ -971,7 +972,6 @@ export const dict = {
|
||||
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/ar.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} من {{total}} أسئلة",
|
||||
"common.review": "مراجعة",
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "Usar idioma: {{language}}",
|
||||
|
||||
"command.session.new": "Nova sessão",
|
||||
"command.session.new.task": "Nova tarefa",
|
||||
"command.file.open": "Abrir arquivo",
|
||||
"command.tab.close": "Fechar aba",
|
||||
"command.context.addSelection": "Adicionar seleção ao contexto",
|
||||
@@ -985,7 +986,6 @@ export const dict = {
|
||||
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/br.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} de {{total}} perguntas",
|
||||
"common.review": "Revisar",
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "Koristi jezik: {{language}}",
|
||||
|
||||
"command.session.new": "Nova sesija",
|
||||
"command.session.new.task": "Novi zadatak",
|
||||
"command.file.open": "Otvori datoteku",
|
||||
"command.tab.close": "Zatvori karticu",
|
||||
"command.context.addSelection": "Dodaj odabir u kontekst",
|
||||
@@ -984,7 +985,6 @@ export const dict = {
|
||||
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/bs.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} od {{total}} pitanja",
|
||||
"common.review": "Pregled",
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "Brug sprog: {{language}}",
|
||||
|
||||
"command.session.new": "Ny session",
|
||||
"command.session.new.task": "Ny opgave",
|
||||
"command.file.open": "Åbn fil",
|
||||
"command.tab.close": "Luk fane",
|
||||
"command.context.addSelection": "Tilføj markering til kontekst",
|
||||
@@ -980,7 +981,6 @@ export const dict = {
|
||||
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/da.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} af {{total}} spørgsmål",
|
||||
"common.review": "Gennemgå",
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ export const dict = {
|
||||
"command.language.set": "Sprache verwenden: {{language}}",
|
||||
|
||||
"command.session.new": "Neue Sitzung",
|
||||
"command.session.new.task": "Neue Aufgabe",
|
||||
"command.file.open": "Datei öffnen",
|
||||
"command.tab.close": "Tab schließen",
|
||||
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
|
||||
@@ -997,7 +998,6 @@ export const dict = {
|
||||
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/de.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} von {{total}} Fragen",
|
||||
"common.review": "Überprüfen",
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "Use language: {{language}}",
|
||||
|
||||
"command.session.new": "New session",
|
||||
"command.session.new.task": "New task",
|
||||
"command.file.open": "Open file",
|
||||
"command.tab.close": "Close tab",
|
||||
"command.context.addSelection": "Add selection to context",
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "Usar idioma: {{language}}",
|
||||
|
||||
"command.session.new": "Nueva sesión",
|
||||
"command.session.new.task": "Nueva tarea",
|
||||
"command.file.open": "Abrir archivo",
|
||||
"command.tab.close": "Cerrar pestaña",
|
||||
"command.context.addSelection": "Añadir selección al contexto",
|
||||
@@ -987,7 +988,6 @@ export const dict = {
|
||||
"profile.personalAccount": "Cuenta personal",
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/es.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} de {{total}} preguntas",
|
||||
"common.review": "Revisar",
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "Utiliser la langue : {{language}}",
|
||||
|
||||
"command.session.new": "Nouvelle session",
|
||||
"command.session.new.task": "Nouvelle tâche",
|
||||
"command.file.open": "Ouvrir un fichier",
|
||||
"command.tab.close": "Fermer l'onglet",
|
||||
"command.context.addSelection": "Ajouter la sélection au contexte",
|
||||
@@ -71,7 +72,8 @@ export const dict = {
|
||||
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
||||
"command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications",
|
||||
"command.workspace.toggle": "Basculer les espaces de travail",
|
||||
"command.workspace.toggle.description": "Activer ou désactiver les espaces de travail multiples dans la barre latérale",
|
||||
"command.workspace.toggle.description":
|
||||
"Activer ou désactiver les espaces de travail multiples dans la barre latérale",
|
||||
"command.session.undo": "Annuler",
|
||||
"command.session.undo.description": "Annuler le dernier message",
|
||||
"command.session.redo": "Rétablir",
|
||||
@@ -995,7 +997,6 @@ export const dict = {
|
||||
"profile.personalAccount": "Compte personnel",
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/fr.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} sur {{total}} questions",
|
||||
"common.review": "Réviser",
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "言語を使用: {{language}}",
|
||||
|
||||
"command.session.new": "新しいセッション",
|
||||
"command.session.new.task": "新しいタスク",
|
||||
"command.file.open": "ファイルを開く",
|
||||
"command.tab.close": "タブを閉じる",
|
||||
"command.context.addSelection": "選択範囲をコンテキストに追加",
|
||||
@@ -979,7 +980,6 @@ export const dict = {
|
||||
"profile.personalAccount": "個人アカウント",
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/ja.ts
|
||||
|
||||
|
||||
"question.summary": "{{total}} 問中 {{n}} 問目",
|
||||
"common.review": "確認",
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ export const dict = {
|
||||
"command.language.set": "언어 사용: {{language}}",
|
||||
|
||||
"command.session.new": "새 세션",
|
||||
"command.session.new.task": "새 작업",
|
||||
"command.file.open": "파일 열기",
|
||||
"command.tab.close": "탭 닫기",
|
||||
"command.context.addSelection": "선택 영역을 컨텍스트에 추가",
|
||||
@@ -976,7 +977,6 @@ export const dict = {
|
||||
"profile.personalAccount": "개인 계정",
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/ko.ts
|
||||
|
||||
|
||||
"question.summary": "{{total}}개 질문 중 {{n}}번째",
|
||||
"common.review": "검토",
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ export const dict = {
|
||||
"command.language.set": "Bruk språk: {{language}}",
|
||||
|
||||
"command.session.new": "Ny sesjon",
|
||||
"command.session.new.task": "Ny oppgave",
|
||||
"command.file.open": "Åpne fil",
|
||||
"command.tab.close": "Lukk fane",
|
||||
"command.context.addSelection": "Legg til markering i kontekst",
|
||||
@@ -981,7 +982,6 @@ export const dict = {
|
||||
"profile.personalAccount": "Personlig konto",
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/no.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} av {{total}} spørsmål",
|
||||
"common.review": "Gjennomgå",
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "Użyj języka: {{language}}",
|
||||
|
||||
"command.session.new": "Nowa sesja",
|
||||
"command.session.new.task": "Nowe zadanie",
|
||||
"command.file.open": "Otwórz plik",
|
||||
"command.tab.close": "Zamknij kartę",
|
||||
"command.context.addSelection": "Dodaj zaznaczenie do kontekstu",
|
||||
@@ -982,7 +983,6 @@ export const dict = {
|
||||
"profile.personalAccount": "Konto osobiste",
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/pl.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} z {{total}} pytań",
|
||||
"common.review": "Przejrzyj",
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "Использовать язык: {{language}}",
|
||||
|
||||
"command.session.new": "Новая сессия",
|
||||
"command.session.new.task": "Новая задача",
|
||||
"command.file.open": "Открыть файл",
|
||||
"command.tab.close": "Закрыть вкладку",
|
||||
"command.context.addSelection": "Добавить выделение в контекст",
|
||||
@@ -984,7 +985,6 @@ export const dict = {
|
||||
"profile.personalAccount": "Личный аккаунт",
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/ru.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} из {{total}} вопросов",
|
||||
"common.review": "Просмотр",
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export const dict = {
|
||||
"command.language.set": "ใช้ภาษา: {{language}}",
|
||||
|
||||
"command.session.new": "เซสชันใหม่",
|
||||
"command.session.new.task": "งานใหม่",
|
||||
"command.file.open": "เปิดไฟล์",
|
||||
"command.tab.close": "ปิดแท็บ",
|
||||
"command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท",
|
||||
@@ -972,7 +973,6 @@ export const dict = {
|
||||
"profile.personalAccount": "บัญชีส่วนตัว",
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/th.ts
|
||||
|
||||
|
||||
"question.summary": "{{n}} จาก {{total}} คำถาม",
|
||||
"common.review": "ตรวจสอบ",
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ export const dict = {
|
||||
"command.language.set": "使用语言:{{language}}",
|
||||
|
||||
"command.session.new": "新建会话",
|
||||
"command.session.new.task": "新建任务",
|
||||
"command.file.open": "打开文件",
|
||||
"command.tab.close": "关闭标签页",
|
||||
"command.context.addSelection": "将所选内容添加到上下文",
|
||||
@@ -967,7 +968,6 @@ export const dict = {
|
||||
"profile.personalAccount": "个人账户",
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/zh.ts
|
||||
|
||||
|
||||
"question.summary": "第 {{n}} / {{total}} 个问题",
|
||||
"common.review": "审查",
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ export const dict = {
|
||||
"command.language.set": "使用語言:{{language}}",
|
||||
|
||||
"command.session.new": "新增工作階段",
|
||||
"command.session.new.task": "新增任務",
|
||||
"command.file.open": "開啟檔案",
|
||||
"command.tab.close": "關閉分頁",
|
||||
"command.context.addSelection": "將選取內容加入上下文",
|
||||
@@ -968,7 +969,6 @@ export const dict = {
|
||||
"profile.personalAccount": "個人帳戶",
|
||||
// Agent Manager strings live in webview-ui/agent-manager/i18n/zht.ts
|
||||
|
||||
|
||||
"question.summary": "第 {{n}} / {{total}} 個問題",
|
||||
"common.review": "審查",
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
*/
|
||||
|
||||
import type { Meta, StoryObj } from "storybook-solidjs-vite"
|
||||
import { StoryProviders } from "./StoryProviders"
|
||||
import { StoryProviders, mockSessionValue } from "./StoryProviders"
|
||||
import { ChatView } from "../components/chat/ChatView"
|
||||
import { QuestionDock } from "../components/chat/QuestionDock"
|
||||
import { SessionContext } from "../context/session"
|
||||
import type { QuestionRequest } from "../types/messages"
|
||||
|
||||
const SESSION_ID = "story-session-chat-001"
|
||||
@@ -69,7 +70,7 @@ const multiQuestion: QuestionRequest = {
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Chat",
|
||||
parameters: { layout: "padded" },
|
||||
parameters: { layout: "fullscreen" },
|
||||
}
|
||||
export default meta
|
||||
type Story = StoryObj
|
||||
@@ -82,13 +83,35 @@ export const ChatViewIdle: Story = {
|
||||
name: "ChatView — idle (empty)",
|
||||
render: () => (
|
||||
<StoryProviders sessionID={SESSION_ID} status="idle">
|
||||
<div style={{ width: "420px", height: "600px", display: "flex", "flex-direction": "column" }}>
|
||||
<div style={{ width: "100%", height: "600px", display: "flex", "flex-direction": "column" }}>
|
||||
<ChatView />
|
||||
</div>
|
||||
</StoryProviders>
|
||||
),
|
||||
}
|
||||
|
||||
/** ChatView with messages — shows the full-width "New task" button above the prompt */
|
||||
export const ChatViewWithMessages: Story = {
|
||||
name: "ChatView — with messages (shows New Task button)",
|
||||
render: () => {
|
||||
const session = {
|
||||
...mockSessionValue({ id: SESSION_ID, status: "idle" }),
|
||||
messages: () => [{ id: "msg-001" }] as any[],
|
||||
totalCost: () => 0.0012,
|
||||
contextUsage: () => ({ tokens: 512, percentage: 6 }),
|
||||
}
|
||||
return (
|
||||
<StoryProviders sessionID={SESSION_ID} status="idle" noPadding>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<div style={{ width: "100%", height: "200px", display: "flex", "flex-direction": "column" }}>
|
||||
<ChatView />
|
||||
</div>
|
||||
</SessionContext.Provider>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// QuestionDock stories
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -97,7 +120,7 @@ export const QuestionDockSingle: Story = {
|
||||
name: "QuestionDock — single question",
|
||||
render: () => (
|
||||
<StoryProviders sessionID={SESSION_ID} questions={[singleQuestion]}>
|
||||
<div style={{ width: "420px" }}>
|
||||
<div style={{ width: "100%" }}>
|
||||
<QuestionDock request={singleQuestion} />
|
||||
</div>
|
||||
</StoryProviders>
|
||||
@@ -108,7 +131,7 @@ export const QuestionDockMulti: Story = {
|
||||
name: "QuestionDock — multi-question wizard",
|
||||
render: () => (
|
||||
<StoryProviders sessionID={SESSION_ID} questions={[multiQuestion]}>
|
||||
<div style={{ width: "420px" }}>
|
||||
<div style={{ width: "100%" }}>
|
||||
<QuestionDock request={multiQuestion} />
|
||||
</div>
|
||||
</StoryProviders>
|
||||
|
||||
@@ -231,6 +231,23 @@ const questionToolPart: ToolPart = {
|
||||
},
|
||||
}
|
||||
|
||||
const questionDismissedPart: ToolPart = {
|
||||
id: "part-question-dismissed-001",
|
||||
sessionID: SESSION_ID,
|
||||
messageID: ASST_MSG_ID,
|
||||
type: "tool",
|
||||
callID: "call-question-dismissed-001",
|
||||
tool: "question",
|
||||
state: {
|
||||
status: "error",
|
||||
input: { question: "Which testing framework?", options: [] },
|
||||
error: "Error: User dismissed this question",
|
||||
title: "Question dismissed",
|
||||
metadata: {},
|
||||
time: { start: now - 2000, end: now - 1500 },
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -244,9 +261,7 @@ function dataWith(parts: any[], permissions?: PermissionRequest[]) {
|
||||
part: {
|
||||
[ASST_MSG_ID]: parts,
|
||||
},
|
||||
permission: permissions
|
||||
? { [SESSION_ID]: permissions }
|
||||
: {},
|
||||
permission: permissions ? { [SESSION_ID]: permissions } : {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,9 +333,7 @@ export const PermissionDock: Story = {
|
||||
>
|
||||
<Show when={perm.patterns.length > 0}>
|
||||
<div class="permission-dock-patterns">
|
||||
<For each={perm.patterns}>
|
||||
{(pattern) => <code class="permission-dock-pattern">{pattern}</code>}
|
||||
</For>
|
||||
<For each={perm.patterns}>{(pattern) => <code class="permission-dock-pattern">{pattern}</code>}</For>
|
||||
</div>
|
||||
</Show>
|
||||
</BasicTool>
|
||||
@@ -461,3 +474,19 @@ export const InlineQuestion: Story = {
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 9. Dismissed question (right-aligned "Questions dismissed" text)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const QuestionDismissed: Story = {
|
||||
name: "Question Dismissed",
|
||||
render: () => {
|
||||
const data = dataWith([textPart, questionDismissedPart])
|
||||
return (
|
||||
<StoryProviders data={data} sessionID={SESSION_ID}>
|
||||
<AssistantMessage message={baseAssistantMessage} />
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ModelSelectorBase } from "../components/shared/ModelSelector"
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Shared",
|
||||
parameters: { layout: "padded" },
|
||||
parameters: { layout: "fullscreen" },
|
||||
}
|
||||
export default meta
|
||||
type Story = StoryObj
|
||||
|
||||
@@ -41,6 +41,11 @@
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
/* New Task Button */
|
||||
.new-task-button-wrapper {
|
||||
padding: 8px 12px 0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Message List
|
||||
============================================ */
|
||||
@@ -907,6 +912,10 @@
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.vscode-session-turn + .vscode-session-turn {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.vscode-session-turn-user {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -932,14 +941,10 @@
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Question Prompt
|
||||
Question Prompt (VS Code overrides)
|
||||
============================================ */
|
||||
|
||||
[data-component="question-prompt"] {
|
||||
[data-slot="question-header"] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[data-component="dock-prompt"][data-kind="question"] {
|
||||
[data-slot="question-progress-nav"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -967,12 +972,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="question-footer"] {
|
||||
padding: 8px;
|
||||
padding-top: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
[data-slot="question-footer"] > [data-component="button"][data-variant="ghost"] {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
@@ -992,155 +991,11 @@
|
||||
color: var(--text-weaker);
|
||||
}
|
||||
|
||||
[data-slot="question-text"] {
|
||||
font-size: var(--font-size-base);
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
[data-slot="question-hint"] {
|
||||
font-size: var(--font-size-small);
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
[data-slot="question-options"] {
|
||||
margin-top: 8px;
|
||||
gap: 4px;
|
||||
padding: 1px 1px 4px;
|
||||
}
|
||||
|
||||
[data-slot="question-option"] {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
background-color: var(--surface-raised-stronger-non-alpha);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: none;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
|
||||
&:hover:not([data-picked="true"]) {
|
||||
background-color: var(--background-base);
|
||||
}
|
||||
|
||||
&[data-picked="true"] {
|
||||
background-color: var(--surface-interactive-weak);
|
||||
border-color: transparent;
|
||||
box-shadow: var(--shadow-xs-border-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="question-option-check"] {
|
||||
display: inline-flex;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
[data-slot="question-option-box"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-weak-base);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background-color: transparent;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
|
||||
[data-component="icon"] {
|
||||
opacity: 0;
|
||||
color: var(--icon-base);
|
||||
}
|
||||
|
||||
&[data-type="radio"] {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
[data-slot="question-option-radio-dot"] {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background-color: var(--background-stronger);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&[data-picked="true"] {
|
||||
border-color: var(--icon-interactive-base);
|
||||
|
||||
[data-component="icon"] {
|
||||
opacity: 1;
|
||||
color: var(--icon-invert-base);
|
||||
}
|
||||
|
||||
&[data-type="checkbox"] {
|
||||
background-color: var(--icon-interactive-base);
|
||||
}
|
||||
|
||||
&[data-type="radio"] {
|
||||
background-color: var(--icon-interactive-base);
|
||||
[data-slot="question-option-radio-dot"] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="question-option-main"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
[data-slot="option-label"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
[data-slot="option-description"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
color: var(--text-base);
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
[data-slot="question-option"][data-custom="true"] {
|
||||
[data-slot="option-description"] {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="question-review"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 4px 2px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
[data-slot="review-title"] {
|
||||
|
||||
@@ -273,6 +273,7 @@ export namespace Server {
|
||||
Bus, // kilocode_change
|
||||
SessionCreatedEvent: Session.Event.Created, // kilocode_change
|
||||
Identifier, // kilocode_change
|
||||
ModelCache, // kilocode_change
|
||||
}),
|
||||
)
|
||||
// kilocode_change end
|
||||
|
||||
@@ -121,3 +121,52 @@ test("model fetch without OAuth accountId does not set kilocodeOrganizationId",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("ModelCache.clear removes cached entry so next fetch hits the network", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://app.kilo.ai/config.json",
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
init: async () => {
|
||||
await Auth.set("kilo", {
|
||||
type: "oauth",
|
||||
access: "token-clear-test",
|
||||
refresh: "refresh-clear",
|
||||
expires: Date.now() + 3600000,
|
||||
accountId: "org-clear",
|
||||
})
|
||||
},
|
||||
fn: async () => {
|
||||
// Populate cache
|
||||
captured = undefined
|
||||
ModelCache.clear("kilo")
|
||||
await ModelCache.fetch("kilo")
|
||||
expect(captured).toBeDefined()
|
||||
|
||||
// Verify cache is populated — second fetch should NOT call fetchKiloModels
|
||||
captured = undefined
|
||||
await ModelCache.fetch("kilo")
|
||||
expect(captured).toBeUndefined()
|
||||
expect(ModelCache.get("kilo")).toBeDefined()
|
||||
|
||||
// Clear the cache
|
||||
ModelCache.clear("kilo")
|
||||
|
||||
// get() should return undefined after clear
|
||||
expect(ModelCache.get("kilo")).toBeUndefined()
|
||||
|
||||
// Next fetch should call fetchKiloModels again
|
||||
captured = undefined
|
||||
await ModelCache.fetch("kilo")
|
||||
expect(captured).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
}
|
||||
|
||||
&.subagent-link {
|
||||
cursor: pointer;
|
||||
color: var(--text-interactive-base);
|
||||
text-decoration: none;
|
||||
text-underline-offset: 2px;
|
||||
|
||||
@@ -964,22 +964,29 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
|
||||
const render = ToolRegistry.render(part.tool) ?? GenericTool
|
||||
|
||||
const dismissed = createMemo(
|
||||
() =>
|
||||
part.tool === "question" &&
|
||||
part.state.status === "error" &&
|
||||
typeof part.state.error === "string" &&
|
||||
part.state.error.includes("dismissed this question"),
|
||||
)
|
||||
|
||||
return (
|
||||
<Show when={!hideQuestion()}>
|
||||
<Show when={dismissed()}>
|
||||
<div style="width: 100%; display: flex; justify-content: flex-end; padding: 4px 8px 4px 0;">
|
||||
<span class="text-13-regular text-text-weak cursor-default">
|
||||
{i18n.t("ui.tool.questions")} dismissed
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!dismissed()}>
|
||||
<div data-component="tool-part-wrapper">
|
||||
<Switch>
|
||||
<Match when={part.state.status === "error" && part.state.error}>
|
||||
{(error) => {
|
||||
const cleaned = error().replace("Error: ", "")
|
||||
if (part.tool === "question" && cleaned.includes("dismissed this question")) {
|
||||
return (
|
||||
<div style="width: 100%; display: flex; justify-content: flex-end;">
|
||||
<span class="text-13-regular text-text-weak cursor-default">
|
||||
{i18n.t("ui.tool.questions")} dismissed
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const [title, ...rest] = cleaned.split(": ")
|
||||
return (
|
||||
<Card variant="error">
|
||||
@@ -1016,6 +1023,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user