mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge branch 'main' into fix/tui-autocomplete-description-spacing
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show underlying models for every Auto model in the model selector when the catalog includes them.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Automatically expose broad reasoning effort options for custom provider models and link saved providers to advanced JSON configuration.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/kilo-ui": patch
|
||||
---
|
||||
|
||||
Improve custom provider edit dialog layout, make advanced configuration action prominent, and add bulk toggle buttons for reasoning and image modalities across all models.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Disable the suggest tool and auto-dismiss pending suggestions in non-interactive CLI runs to prevent hanging on benchmarks and automated pipelines.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Exclude ChatGPT subscriptions from explicit prompt cache breakpoints.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Prevent project MCP configs from resolving variable-backed headers or inheriting trusted headers when changing endpoints, while preserving unaffected servers.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/kilo-memory": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Reduce noisy memory timeout warnings and retry transient background consolidation failures once.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@kilocode/kilo-gateway": patch
|
||||
"@kilocode/cli": patch
|
||||
"kilo-code": patch
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Ignore negative pricing entries from model catalogs and handle unpriced models gracefully in UI price formatting.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Fix Mermaid Copy PNG/SVG in VS Code webviews so they put images on the clipboard instead of failing or copying SVG markup.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Set explicit prompt cache breakpoints on stable prefixes for OpenAI GPT-5.6+ models.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support browsing an instance's model catalog before a session starts.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep model and provider errors visible in VS Code when chat history refreshes.
|
||||
@@ -31,7 +31,7 @@ Selecting a suggestion inserts the mention and highlights it in the input. File
|
||||
|
||||
### Referencing Past Chats
|
||||
|
||||
Choosing **Past chats** from the `@` menu opens a searchable picker of sessions in the current workspace, ordered by recency. Search matches session titles and workspace names. Selecting a session inserts a highlighted mention token; when you send the message, that session's current chat history is added as context so the agent can build on the earlier conversation. Clicking the mention token opens the referenced session.
|
||||
Choosing **Past chats** from the `@` menu opens a searchable picker of sessions in the current workspace and related Git worktrees, including Agent Manager worktrees, ordered by recency. Search matches session titles and worktree names. Selecting a session inserts a highlighted mention token; when you send the message, that session's current chat history is added as context so the agent can build on the earlier conversation. Clicking the mention token opens the referenced session.
|
||||
|
||||
Very long chat histories are truncated, keeping the beginning and end, so a single mention cannot fill the context window.
|
||||
|
||||
@@ -77,7 +77,7 @@ This means the agent can explore your entire project as needed, rather than bein
|
||||
| **Mention files when helpful** | If you know the exact file, mention its path to save the agent a search step |
|
||||
| **Keep editor tabs relevant** | Open tabs are passed as context, so keep relevant files open |
|
||||
| **Trust the agent's tools** | The agent can search, read, and explore your codebase — let it do the discovery work |
|
||||
| **Reference a past chat** | Type `@` and choose **Past chats** to search sessions in the current workspace. Selecting a session adds its current chat history as context when you send the message. |
|
||||
| **Reference a past chat** | Type `@` and choose **Past chats** to search the current workspace and related Git worktrees, including Agent Manager worktrees. Selecting a session adds its current chat history as context when you send the message. |
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="CLI" %}
|
||||
|
||||
@@ -21,9 +21,9 @@ Different search surfaces cover different content and scopes:
|
||||
| [JetBrains History](#search-session-history-in-jetbrains) | Session titles | Sessions loaded in the selected Local or Cloud history view |
|
||||
| [CLI Past chats](#reference-a-past-chat-in-the-cli-tui) | Session titles | Current workspace |
|
||||
| [`kilo session list --search`](#filter-session-titles) | Session titles | Current workspace, or every local workspace with `--all` |
|
||||
| [Ask Kilo to recall past chats](#ask-kilo-to-search-past-chats) | Titles and high-signal chat content | Current workspace |
|
||||
| [Ask Kilo to recall past chats](#ask-kilo-to-search-past-chats) | Titles and high-signal chat content | Current workspace and related Git worktrees |
|
||||
| [VS Code chat search](#search-the-open-chat-session) | Rendered content in the open session | Current chat session |
|
||||
| [VS Code Past chats](#add-a-past-chat-to-the-current-session) | Session titles and workspace names | Current workspace |
|
||||
| [VS Code Past chats](#add-a-past-chat-to-the-current-session) | Session titles and worktree names | Current workspace and related Git worktrees, including Agent Manager worktrees |
|
||||
| [SQLite query](#query-sqlite-directly) | Any stored field you select | Entire selected local database |
|
||||
|
||||
History and CLI list searches do **not** search message content. To find a string inside prompts or replies, ask Kilo to search past chats or use a direct SQLite query.
|
||||
@@ -58,7 +58,7 @@ Kilo loads older messages while the search is active so the search covers the fu
|
||||
|
||||
### Add a past chat to the current session
|
||||
|
||||
Type `@` in the chat input, select **Past chats**, and search by session title or workspace name. Selecting a result adds that past chat's content as context when you send the message from your current session. It does not switch or reopen sessions. Past chats includes sessions in the current workspace.
|
||||
Type `@` in the chat input, select **Past chats**, and search by session title or worktree name. Selecting a result adds that past chat's content as context when you send the message from your current session. It does not switch or reopen sessions. Past chats includes sessions in the current workspace and related Git worktrees, including Agent Manager worktrees.
|
||||
|
||||
{% /tab %}
|
||||
{% tab label="JetBrains" %}
|
||||
@@ -98,7 +98,7 @@ The simplest way to search message content across past local sessions is to ask
|
||||
Search my local sessions for "database disk image is malformed" and summarize the matching conversations.
|
||||
```
|
||||
|
||||
Kilo's local recall search covers session titles, user and assistant text, file references, and failed tool errors in the current workspace. Every query term must occur somewhere in a matching session; exact phrases and user-authored matches rank higher.
|
||||
Kilo's local recall search covers session titles, user and assistant text, file references, and failed tool errors in the current workspace and related Git worktrees. Every query term must occur somewhere in a matching session; exact phrases and user-authored matches rank higher.
|
||||
|
||||
The search includes archived and child sessions. It excludes reasoning, synthetic or ignored text, successful tool output, file contents, and other metadata. Kilo can then read the full chat history for a selected result.
|
||||
|
||||
@@ -144,8 +144,8 @@ Stable installations normally use these paths:
|
||||
|
||||
| Environment | Default path |
|
||||
|---|---|
|
||||
| Windows | `%LOCALAPPDATA%\kilo\kilo.db` |
|
||||
| macOS | `~/Library/Application Support/kilo/kilo.db` |
|
||||
| Windows | `%USERPROFILE%\.local\share\kilo\kilo.db` |
|
||||
| macOS | `~/.local/share/kilo/kilo.db` |
|
||||
| Linux | `~/.local/share/kilo/kilo.db` |
|
||||
| VS Code Remote SSH | `~/.local/share/kilo/kilo.db` on the remote machine |
|
||||
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ The default database location depends on where Kilo Code is running:
|
||||
|
||||
| Environment | Database path |
|
||||
|---|---|
|
||||
| Windows | `%LOCALAPPDATA%\kilo\kilo.db` |
|
||||
| macOS | `~/Library/Application Support/kilo/kilo.db` |
|
||||
| Windows | `%USERPROFILE%\.local\share\kilo\kilo.db` |
|
||||
| macOS | `~/.local/share/kilo/kilo.db` |
|
||||
| Linux | `~/.local/share/kilo/kilo.db` |
|
||||
| VS Code Remote SSH | `~/.local/share/kilo/kilo.db` on the remote machine |
|
||||
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f2349f8c0d7049811702e5b8cbeb5fc2f402bdda8630d1a5210a970a13284197
|
||||
size 3694
|
||||
oid sha256:33ee6eb7070237487d1931f32ac77a2549e18e664b993895e9a9d6619527d0aa
|
||||
size 4063
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f2349f8c0d7049811702e5b8cbeb5fc2f402bdda8630d1a5210a970a13284197
|
||||
size 3694
|
||||
oid sha256:33ee6eb7070237487d1931f32ac77a2549e18e664b993895e9a9d6619527d0aa
|
||||
size 4063
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72a0a206e978250d41b42f3c0585691a78cbe2635c12d320bcefe61cd938ced5
|
||||
size 280
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72a0a206e978250d41b42f3c0585691a78cbe2635c12d320bcefe61cd938ced5
|
||||
size 280
|
||||
@@ -76,7 +76,7 @@ type OpenRouterModel = z.infer<typeof openRouterModelSchema>
|
||||
function parseApiPrice(price: string | null | undefined): number | undefined {
|
||||
if (!price) return undefined
|
||||
const parsed = parseFloat(price)
|
||||
if (isNaN(parsed)) return undefined
|
||||
if (isNaN(parsed) || parsed < 0) return undefined
|
||||
return parsed * 1_000_000 // Convert $/token → $/M tokens
|
||||
}
|
||||
|
||||
|
||||
@@ -383,3 +383,66 @@ test("keeps organization catalog errors from silently falling back to personal m
|
||||
expect(calls[0]).toContain("/api/gateway/transcription-models")
|
||||
expect(headers[0]?.get("X-KILOCODE-ORGANIZATIONID")).toBe("org-1")
|
||||
})
|
||||
|
||||
test("omits cost when pricing contains negative values (dynamic/auto-routed pricing)", async () => {
|
||||
const orig = globalThis.fetch
|
||||
stubFetch(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: "openrouter/auto",
|
||||
name: "Auto Router",
|
||||
context_length: 128000,
|
||||
max_completion_tokens: 16384,
|
||||
architecture: {
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
},
|
||||
supported_parameters: ["tools"],
|
||||
pricing: {
|
||||
prompt: "-1",
|
||||
completion: "-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "test/fixed-price",
|
||||
name: "Fixed Price Model",
|
||||
context_length: 128000,
|
||||
max_completion_tokens: 16384,
|
||||
architecture: {
|
||||
input_modalities: ["text"],
|
||||
output_modalities: ["text"],
|
||||
},
|
||||
supported_parameters: ["tools"],
|
||||
pricing: {
|
||||
prompt: "0.000003",
|
||||
completion: "0.000015",
|
||||
input_cache_read: "0.0000003",
|
||||
input_cache_write: "-1",
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
const result = await fetchKiloModels({})
|
||||
|
||||
;(globalThis as any).fetch = orig
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.models["openrouter/auto"]).toBeDefined()
|
||||
expect(result.models["openrouter/auto"].cost).toBeUndefined()
|
||||
expect(result.models["test/fixed-price"].cost).toEqual({
|
||||
input: 3,
|
||||
output: 15,
|
||||
cache_read: 0.3,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
+3
-3
@@ -157,14 +157,14 @@ internal class ModelDetailsPanel(
|
||||
return buildList {
|
||||
item.releaseDate?.let { add(KiloBundle.message("model.picker.details.released") to date(it)) }
|
||||
if (!item.free) {
|
||||
item.cost?.let { cost ->
|
||||
item.cost?.takeIf { it.input > 0.0 || it.output > 0.0 }?.let { cost ->
|
||||
add(KiloBundle.message("model.picker.details.input") to price(cost.input))
|
||||
add(KiloBundle.message("model.picker.details.output") to price(cost.output))
|
||||
add(KiloBundle.message("model.picker.details.cached") to cached(cost.input, cost.cache?.read))
|
||||
add(KiloBundle.message("model.picker.details.average") to price(average(cost.input, cost.output, cost.cache?.read)))
|
||||
} ?: run {
|
||||
item.inputPrice?.let { add(KiloBundle.message("model.picker.details.input") to price(it)) }
|
||||
item.outputPrice?.let { add(KiloBundle.message("model.picker.details.output") to price(it)) }
|
||||
item.inputPrice?.takeIf { it > 0.0 }?.let { add(KiloBundle.message("model.picker.details.input") to price(it)) }
|
||||
item.outputPrice?.takeIf { it > 0.0 }?.let { add(KiloBundle.message("model.picker.details.output") to price(it)) }
|
||||
}
|
||||
}
|
||||
ctx?.let { add(KiloBundle.message("model.picker.details.context") to context(it)) }
|
||||
|
||||
@@ -126,7 +126,7 @@ export namespace MemoryOperations {
|
||||
const adds = input.ops
|
||||
.filter((item): item is Add => item.action === "add")
|
||||
// Redact rejected text too: this filter runs before the secret one, so a rejected op never
|
||||
// reaches it, and skips flow into the persistent decisions audit (/memory/show, TUI).
|
||||
// reaches it, and skips remain visible in the operation result.
|
||||
.filter((op) => {
|
||||
const item = reject(op)
|
||||
if (!item) return true
|
||||
@@ -314,7 +314,7 @@ export namespace MemoryOperations {
|
||||
}
|
||||
}
|
||||
|
||||
async function persist(input: { root: string; state: MemorySchema.State; count: number; removed: number }) {
|
||||
async function persist(input: { root: string; state: MemorySchema.State; count: number }) {
|
||||
const index = await MemoryIndexer.rebuild({ root: input.root, state: input.state })
|
||||
await MemoryFiles.writeState(input.root, {
|
||||
...input.state,
|
||||
@@ -323,7 +323,6 @@ export namespace MemoryOperations {
|
||||
lastOperationCount: input.count,
|
||||
},
|
||||
})
|
||||
await MemoryFiles.append(input.root, `apply ops=${input.count} removed=${input.removed}`)
|
||||
return index
|
||||
}
|
||||
|
||||
@@ -365,9 +364,9 @@ export namespace MemoryOperations {
|
||||
const prepared = prepare({ state, ops: input.ops, max: state.limits.maxLineChars })
|
||||
const removes = input.ops.filter((item): item is Remove => item.action === "remove")
|
||||
const plan = planOps({ docs, inventory, removes, adds: prepared.adds, now: Date.now() })
|
||||
// Commit (IO): write changed documents, then rebuild the index, persist state, and audit.
|
||||
// Commit (IO): write changed documents, then rebuild the index and persist state.
|
||||
await writeDocs({ root: input.root, plan })
|
||||
const index = await persist({ root: input.root, state, count: plan.count, removed: plan.removed })
|
||||
const index = await persist({ root: input.root, state, count: plan.count })
|
||||
return {
|
||||
operationCount: plan.count,
|
||||
added: plan.added,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { MemoryOperations } from "./operations"
|
||||
import { MemoryRedact } from "./redact"
|
||||
import { MemoryShared } from "../recall/shared"
|
||||
import { TRANSIENT } from "../schema"
|
||||
import type { MemoryFiles } from "../storage/store"
|
||||
import type { CaptureSkip } from "./parse"
|
||||
|
||||
export { TRANSIENT }
|
||||
|
||||
export type CaptureSourceItem = {
|
||||
id: string
|
||||
text: string
|
||||
@@ -69,14 +72,75 @@ export function errorReason(err: unknown) {
|
||||
return MemoryShared.brief(MemoryRedact.text(parts.join(" ")), 500)
|
||||
}
|
||||
|
||||
export function guardReason(input: string) {
|
||||
const value = input.toLowerCase()
|
||||
if (/\b(429|rate[_ -]?limit|too many requests)\b/.test(value)) return "rate_limit_guard"
|
||||
if (/\b(insufficient[_ -]?quota|quota exceeded|exceeded your quota|billing|credits?|credit balance)\b/.test(value))
|
||||
const RATE_TEXT = /\b(429|rate[_ -]?limit|too many requests)\b/
|
||||
const QUOTA_TEXT = /\b(insufficient[_ -]?quota|quota exceeded|exceeded your quota|billing|credits?|credit balance)\b/
|
||||
const TIMEOUT_TEXT =
|
||||
/\b(timeouterror|etimedout|deadline[ _-]?exceeded|timed out|(connect|headers|body|gateway)[ _-]?time[ -]?out)\b|^timeout$/
|
||||
const TIMEOUT_CODES = new Set(["ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"])
|
||||
|
||||
/** Structural timeout detection over an error's name/status/code, its message text, and its nested
|
||||
* cause/errors chains (cycle-safe). Message matching is limited to `message` — never the serialized
|
||||
* body/data blobs — so response payloads that merely mention "timeout" cannot classify as transient. */
|
||||
function timedOut(item: unknown, seen = new WeakSet<object>()): boolean {
|
||||
if (!item || typeof item !== "object" || seen.has(item)) return false
|
||||
seen.add(item)
|
||||
const err = item as {
|
||||
name?: unknown
|
||||
code?: unknown
|
||||
status?: unknown
|
||||
statusCode?: unknown
|
||||
message?: unknown
|
||||
cause?: unknown
|
||||
errors?: unknown
|
||||
}
|
||||
if (err.name === "TimeoutError") return true
|
||||
if (err.status === 504 || err.statusCode === 504) return true
|
||||
if (typeof err.code === "string" && TIMEOUT_CODES.has(err.code)) return true
|
||||
if (typeof err.message === "string" && TIMEOUT_TEXT.test(err.message.toLowerCase())) return true
|
||||
if (timedOut(err.cause, seen)) return true
|
||||
return Array.isArray(err.errors) && err.errors.some((entry) => timedOut(entry, seen))
|
||||
}
|
||||
|
||||
function rateGuarded(item: unknown, seen = new WeakSet<object>()): boolean {
|
||||
if (!item || typeof item !== "object" || seen.has(item)) return false
|
||||
seen.add(item)
|
||||
const err = item as {
|
||||
status?: unknown
|
||||
statusCode?: unknown
|
||||
message?: unknown
|
||||
cause?: unknown
|
||||
errors?: unknown
|
||||
}
|
||||
if (err.status === 429 || err.statusCode === 429) return true
|
||||
if (typeof err.message === "string" && RATE_TEXT.test(err.message.toLowerCase())) return true
|
||||
if (rateGuarded(err.cause, seen)) return true
|
||||
return Array.isArray(err.errors) && err.errors.some((entry) => rateGuarded(entry, seen))
|
||||
}
|
||||
|
||||
function quotaGuarded(item: unknown, seen = new WeakSet<object>()): boolean {
|
||||
if (!item || typeof item !== "object" || seen.has(item)) return false
|
||||
seen.add(item)
|
||||
const err = item as {
|
||||
message?: unknown
|
||||
cause?: unknown
|
||||
errors?: unknown
|
||||
}
|
||||
if (typeof err.message === "string" && QUOTA_TEXT.test(err.message.toLowerCase())) return true
|
||||
if (quotaGuarded(err.cause, seen)) return true
|
||||
return Array.isArray(err.errors) && err.errors.some((entry) => quotaGuarded(entry, seen))
|
||||
}
|
||||
|
||||
export function guardReason(input: unknown) {
|
||||
const value = (typeof input === "string" ? input : errorReason(input)).toLowerCase()
|
||||
if (typeof input === "string" ? RATE_TEXT.test(value) : rateGuarded(input) || RATE_TEXT.test(value))
|
||||
return "rate_limit_guard"
|
||||
if (typeof input === "string" ? QUOTA_TEXT.test(value) : quotaGuarded(input) || QUOTA_TEXT.test(value))
|
||||
return "quota_guard"
|
||||
if (typeof input === "string" ? TIMEOUT_TEXT.test(value) : timedOut(input)) return TRANSIENT
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** @deprecated Memory audit persistence was removed. */
|
||||
export function skipped(input: { sessionID: string; reason: string }): MemoryFiles.Decision {
|
||||
return {
|
||||
kind: "typed",
|
||||
@@ -94,6 +158,7 @@ export function skipped(input: { sessionID: string; reason: string }): MemoryFil
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Memory audit persistence was removed. */
|
||||
export function auditOps(ops: MemoryOperations.Op[]) {
|
||||
return MemoryShared.audit(ops)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Cause, Effect } from "effect"
|
||||
import {
|
||||
auditOps,
|
||||
cap,
|
||||
capturePlan,
|
||||
digestPrompt,
|
||||
@@ -18,9 +17,9 @@ import {
|
||||
parseJson,
|
||||
parseOps,
|
||||
salvageTyped,
|
||||
skipped,
|
||||
summarize,
|
||||
summarizeDiffs,
|
||||
TRANSIENT,
|
||||
typedPrompt,
|
||||
usage,
|
||||
verifySkips,
|
||||
@@ -105,11 +104,16 @@ export namespace MemoryCapture {
|
||||
yield* memory.prepare({ root })
|
||||
const state = yield* memory.state({ root })
|
||||
const reported = new Set<string>()
|
||||
const fail = (reason: string) =>
|
||||
const fail = (reason: string, detail?: string) =>
|
||||
Effect.promise(async () => {
|
||||
const safe = MemoryRedact.text(reason)
|
||||
if (reported.has(safe)) return
|
||||
reported.add(safe)
|
||||
if (reason === TRANSIENT)
|
||||
MemoryLog.warn("memory capture transient failure", {
|
||||
reason: safe,
|
||||
detail: MemoryShared.brief(MemoryRedact.text(detail ?? safe), 160),
|
||||
})
|
||||
await MemoryEvents.publish({
|
||||
event: "error",
|
||||
payload: MemoryEvents.status({
|
||||
@@ -123,7 +127,6 @@ export namespace MemoryCapture {
|
||||
})
|
||||
const skip = (reason: string, opts?: { idleFlush?: boolean }) =>
|
||||
Effect.gen(function* () {
|
||||
if (state.enabled) yield* memory.decide({ root, decision: skipped({ sessionID: input.sessionID, reason }) })
|
||||
yield* Effect.promise(() =>
|
||||
MemoryEvents.publish({
|
||||
event: "status",
|
||||
@@ -203,23 +206,6 @@ export namespace MemoryCapture {
|
||||
tokens: 0,
|
||||
fallback: true,
|
||||
})
|
||||
yield* memory.decide({
|
||||
root,
|
||||
decision: {
|
||||
kind: "digest",
|
||||
trigger: "turn-close",
|
||||
sessionID: input.sessionID,
|
||||
result: "fallback",
|
||||
llm: false,
|
||||
parsed: false,
|
||||
fallback: true,
|
||||
reason: input.reason,
|
||||
tokens: 0,
|
||||
operationCount: 1,
|
||||
skippedCount: 0,
|
||||
summary: `session digest fallback on ${input.reason ?? "close"}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
return yield* skip(plan.skipReason, plan.idleFlush ? { idleFlush: true } : undefined)
|
||||
}
|
||||
@@ -232,22 +218,12 @@ export namespace MemoryCapture {
|
||||
|
||||
const model =
|
||||
digestDue || typedCall
|
||||
? yield* Effect.gen(function* () {
|
||||
const resolution = yield* input.model.resolve({
|
||||
? (
|
||||
yield* input.model.resolve({
|
||||
configured: input.memoryModel,
|
||||
session: view.sessionModel,
|
||||
})
|
||||
if (resolution.fallback) {
|
||||
yield* memory.append({
|
||||
root,
|
||||
text: `memory_model_config reason=${MemoryShared.brief(
|
||||
MemoryRedact.text(resolution.fallback.reason),
|
||||
160,
|
||||
)} fallback=1`,
|
||||
})
|
||||
}
|
||||
return resolution.handle
|
||||
})
|
||||
).handle
|
||||
: undefined
|
||||
const digestEffect = digestDue
|
||||
? Effect.gen(function* () {
|
||||
@@ -277,9 +253,8 @@ export namespace MemoryCapture {
|
||||
Effect.gen(function* () {
|
||||
if (signal.aborted) return { ok: false as const, reason: "cancelled" }
|
||||
const raw = errorReason(err)
|
||||
const reason = MemoryRedact.text(guardReason(raw) ?? raw)
|
||||
yield* fail(reason)
|
||||
yield* memory.append({ root, text: `digest error=${MemoryShared.brief(reason, 160)} fallback=1` })
|
||||
const reason = MemoryRedact.text(guardReason(err) ?? raw)
|
||||
yield* fail(reason, raw)
|
||||
return { ok: false as const, reason }
|
||||
}),
|
||||
),
|
||||
@@ -296,14 +271,9 @@ export namespace MemoryCapture {
|
||||
try: () => parseJson(digestSchema, result.result.text),
|
||||
catch: (error) => error,
|
||||
}).pipe(
|
||||
Effect.catch((err: unknown) =>
|
||||
Effect.catch(() =>
|
||||
Effect.gen(function* () {
|
||||
const reason = MemoryRedact.text(errorReason(err))
|
||||
yield* fail("digest parse_error")
|
||||
yield* memory.append({
|
||||
root,
|
||||
text: `digest parse_error=${MemoryShared.brief(reason, 160)} fallback=1`,
|
||||
})
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
@@ -402,9 +372,8 @@ export namespace MemoryCapture {
|
||||
Effect.gen(function* () {
|
||||
if (signal.aborted) return { ok: false as const, reason: "cancelled" }
|
||||
const raw = errorReason(err)
|
||||
const reason = MemoryRedact.text(guardReason(raw) ?? raw)
|
||||
yield* fail(reason)
|
||||
yield* memory.append({ root, text: `consolidate error=${MemoryShared.brief(reason, 160)}` })
|
||||
const reason = MemoryRedact.text(guardReason(err) ?? raw)
|
||||
yield* fail(reason, raw)
|
||||
return { ok: false as const, reason }
|
||||
}),
|
||||
),
|
||||
@@ -423,11 +392,9 @@ export namespace MemoryCapture {
|
||||
try: () => salvageTyped(result.result.text),
|
||||
catch: (error) => error,
|
||||
}).pipe(
|
||||
Effect.catch((err: unknown) =>
|
||||
Effect.catch(() =>
|
||||
Effect.gen(function* () {
|
||||
const reason = MemoryRedact.text(errorReason(err))
|
||||
yield* fail("consolidate parse_error")
|
||||
yield* memory.append({ root, text: `consolidate parse_error=${MemoryShared.brief(reason, 160)}` })
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
@@ -475,30 +442,6 @@ export namespace MemoryCapture {
|
||||
fallback: Boolean(digest.reason),
|
||||
})
|
||||
}
|
||||
if (digestDue) {
|
||||
yield* memory.decide({
|
||||
root,
|
||||
decision: {
|
||||
kind: "digest",
|
||||
trigger: "turn-close",
|
||||
sessionID: input.sessionID,
|
||||
result: digest.reason ? "fallback" : digest.summary ? "saved" : "skipped",
|
||||
llm: true,
|
||||
parsed: Boolean(digest.summary && !digest.reason),
|
||||
fallback: Boolean(digest.reason),
|
||||
reason: digest.reason,
|
||||
tokens: digest.tokens,
|
||||
operationCount: digest.summary ? 1 : 0,
|
||||
skippedCount: digest.summary ? 0 : 1,
|
||||
summary: digest.reason
|
||||
? `session digest used fallback after ${digest.reason}`
|
||||
: digest.summary
|
||||
? "session digest saved"
|
||||
: "session digest skipped",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Apply adds only: a same-key add supersedes/updates an existing fact in place. reconcile also
|
||||
// surfaces exact-key auto-removes, but V0 keeps hard removes explicit-only — auto-capture never
|
||||
// deletes memory it merely paraphrased (or wrongly flags), so reconciled.removes is not applied.
|
||||
@@ -506,39 +449,11 @@ export namespace MemoryCapture {
|
||||
const ops = reconciled.ops.slice(0, state.capture.maxOpsPerRun)
|
||||
const project =
|
||||
ops.length > 0 ? yield* memory.apply({ root, ops, trigger: "turn-close", tokens: generated.tokens }) : undefined
|
||||
// Apply-time skips (content gate + secret, both redacted at creation) surface in the typed audit
|
||||
// record alongside the model's own declared skips.
|
||||
const applied: CaptureSkip[] = [...generated.skipped, ...(project?.skipped ?? [])]
|
||||
const count = project?.operationCount ?? 0
|
||||
if (typedCall) {
|
||||
yield* memory.decide({
|
||||
root,
|
||||
decision: {
|
||||
kind: "typed",
|
||||
trigger: "turn-close",
|
||||
sessionID: input.sessionID,
|
||||
result: generated.fallback ? "fallback" : count > 0 ? "saved" : "skipped",
|
||||
llm: true,
|
||||
parsed: !generated.fallback,
|
||||
fallback: generated.fallback,
|
||||
reason: generated.reason,
|
||||
tokens: generated.tokens,
|
||||
operationCount: count,
|
||||
skippedCount: applied.length,
|
||||
skipped: applied,
|
||||
operations: auditOps(ops),
|
||||
files: [...new Set(ops.flatMap((item) => (item.action === "add" && item.file ? [item.file] : [])))],
|
||||
summary: generated.fallback
|
||||
? `typed consolidation skipped after ${generated.reason ?? "model failure"}`
|
||||
: count > 0
|
||||
? `typed consolidation saved ${count} ops`
|
||||
: `typed consolidation skipped ${applied.length} candidates`,
|
||||
},
|
||||
})
|
||||
}
|
||||
const tokens = digest.tokens + generated.tokens
|
||||
if (!digest.summary && !typedCall && count === 0) return yield* skip("no_ops")
|
||||
if ((digestDue || typedCall || count > 0) && (!typedCall || !generated.fallback)) {
|
||||
if (digestDue || typedCall || count > 0) {
|
||||
yield* memory.commit({
|
||||
root,
|
||||
now,
|
||||
@@ -546,8 +461,7 @@ export namespace MemoryCapture {
|
||||
tokens,
|
||||
count,
|
||||
digest: Boolean(digest.summary),
|
||||
typed: typedCall,
|
||||
skipped: applied,
|
||||
typed: typedCall && !generated.fallback,
|
||||
})
|
||||
}
|
||||
const updated = yield* memory.state({ root })
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Context, Effect, Layer, Semaphore } from "effect"
|
||||
import { skipLine, type CaptureSkip } from "../capture/capture"
|
||||
import type { Memory } from "../memory"
|
||||
import type { MemoryOperations } from "../capture/operations"
|
||||
import { MemoryRecall } from "../recall/recall"
|
||||
@@ -104,7 +103,6 @@ type CommitInput = RootInput & {
|
||||
// shared typed-interval clock (lastTypedConsolidationAt); a digest-only commit must leave it untouched so a
|
||||
// digest in one session cannot throttle another session's typed capture.
|
||||
typed: boolean
|
||||
skipped: CaptureSkip[]
|
||||
cost?: number
|
||||
}
|
||||
|
||||
@@ -156,10 +154,12 @@ export namespace MemoryService {
|
||||
readonly recent: (
|
||||
input: RecentInput,
|
||||
) => Effect.Effect<Awaited<ReturnType<typeof MemoryFiles.recentSessions>>, Failure>
|
||||
/** @deprecated Memory audit persistence was removed. */
|
||||
readonly append: (input: AppendInput) => Effect.Effect<void, Failure>
|
||||
readonly index: (input: RootInput) => Effect.Effect<Index, Failure>
|
||||
readonly commit: (input: CommitInput) => Effect.Effect<void, Failure>
|
||||
readonly recordRecall: (input: RecordRecallInput) => Effect.Effect<void, Failure>
|
||||
/** @deprecated Memory audit persistence was removed. */
|
||||
readonly decide: (input: DecideInput) => Effect.Effect<void, Failure>
|
||||
readonly readSource: (input: ReadSourceInput) => Effect.Effect<string, Failure>
|
||||
readonly turnLock: (sessionID: SessionID) => Semaphore.Semaphore
|
||||
@@ -200,7 +200,7 @@ export namespace MemoryService {
|
||||
return Object.fromEntries(entries) as Sources
|
||||
}),
|
||||
recent: (input) => bridge(() => MemoryFiles.recentSessions(input.root, input.limit, input.max)),
|
||||
append: (input) => bridge(() => MemoryFiles.append(input.root, input.text)),
|
||||
append: () => Effect.void,
|
||||
index: (input) =>
|
||||
bridge(async () => {
|
||||
const text = await MemoryFiles.readIndex(input.root)
|
||||
@@ -219,20 +219,11 @@ export namespace MemoryService {
|
||||
lastSessionSavedAt: input.digest ? input.now : state.stats.lastSessionSavedAt,
|
||||
lastConsolidatedMessageID: input.messageID,
|
||||
lastConsolidationCost: input.cost ?? state.stats.lastConsolidationCost,
|
||||
lastConsolidationTokens: input.tokens,
|
||||
lastOperationCount: input.count,
|
||||
lastConsolidationTokens:
|
||||
input.typed || input.digest ? input.tokens : state.stats.lastConsolidationTokens,
|
||||
lastOperationCount: input.typed ? input.count : state.stats.lastOperationCount,
|
||||
},
|
||||
})
|
||||
const skip = skipLine(input.skipped)
|
||||
await MemoryFiles.append(
|
||||
input.root,
|
||||
[
|
||||
`consolidate trigger=turn-close digest=${input.digest ? 1 : 0} ops=${input.count} tokens=${input.tokens}`,
|
||||
skip,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
)
|
||||
}),
|
||||
),
|
||||
recordRecall: (input) =>
|
||||
@@ -266,7 +257,7 @@ export namespace MemoryService {
|
||||
}),
|
||||
})
|
||||
}),
|
||||
decide: (input) => bridge(() => MemoryFiles.decide(input.root, input.decision)),
|
||||
decide: () => Effect.void,
|
||||
readSource: (input) => bridge(() => MemoryFiles.readSource(input.root, input.file)),
|
||||
// Ref-counted so every acquirer — in-flight or queued behind `withPermits` — shares one
|
||||
// semaphore. Each call must be balanced by exactly one `dropLock`.
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import { MemoryShared } from "./recall/shared"
|
||||
import type { MemoryOperations } from "./capture/operations"
|
||||
import { MemoryRedact } from "./capture/redact"
|
||||
|
||||
/** Human-facing messages and audit views describing an explicit apply result. */
|
||||
/** Human-facing messages describing an explicit apply result. */
|
||||
export namespace MemoryNotice {
|
||||
export function saved(input: { added: number; removed: number }) {
|
||||
return input.removed > 0 || input.added > 0
|
||||
}
|
||||
|
||||
export function summary(input: { added: number; removed: number; count: number }) {
|
||||
if (input.added > 0 && input.removed > 0) {
|
||||
return `explicit memory operation saved ${input.added} and removed ${input.removed}`
|
||||
}
|
||||
if (input.added > 0) return `explicit memory operation saved ${input.added} ops`
|
||||
if (input.removed > 0) return `explicit memory operation removed ${input.removed} entries`
|
||||
if (input.count > 0) return "explicit memory operation matched no source memory"
|
||||
return "explicit memory operation had no accepted ops"
|
||||
}
|
||||
|
||||
export function message(input: { ops: MemoryOperations.Op[]; added: number; removed: number; count: number }) {
|
||||
const refs = MemoryShared.refs(input.ops)
|
||||
if (input.added > 0 && input.removed > 0) return `Memory updated · ${input.added} saved, ${input.removed} removed`
|
||||
@@ -25,15 +14,4 @@ export namespace MemoryNotice {
|
||||
if (input.removed > 0) return `Memory updated · ${input.removed} removed`
|
||||
return `Memory unchanged · ${input.count} ops`
|
||||
}
|
||||
|
||||
export function skip(input: MemoryOperations.Rejection[]) {
|
||||
return input.map((item) => (item.reason === "out_of_scope" ? { reason: item.reason } : item))
|
||||
}
|
||||
|
||||
export function ops(input: { ops: MemoryOperations.Op[]; skipped: MemoryOperations.Rejection[] }) {
|
||||
const blocked = new Set(input.skipped.filter((item) => item.reason === "out_of_scope").map((item) => item.text))
|
||||
return MemoryShared.audit(
|
||||
input.ops.filter((item) => item.action !== "add" || !blocked.has(MemoryRedact.text(item.text))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { MemorySchema } from "./schema"
|
||||
import { MemoryShared } from "./recall/shared"
|
||||
import { MemoryToken } from "./recall/token"
|
||||
import { MemorySlug } from "./slug"
|
||||
import { MemoryRedact } from "./capture/redact"
|
||||
|
||||
/** Root-bound package facade. External Kilo surfaces should derive root from workspace context first. */
|
||||
export namespace Memory {
|
||||
@@ -97,7 +96,6 @@ export namespace Memory {
|
||||
const state = await MemoryFiles.readState(input.root)
|
||||
const next = { ...state, enabled: false }
|
||||
await MemoryFiles.writeState(input.root, next)
|
||||
await MemoryFiles.append(input.root, `disable ${next.scope} source=command`)
|
||||
return { root: input.root, state: next }
|
||||
})
|
||||
}
|
||||
@@ -124,16 +122,6 @@ export namespace Memory {
|
||||
...(input.settings.verbose === undefined ? {} : { verbose: input.settings.verbose }),
|
||||
}
|
||||
await MemoryFiles.writeState(input.root, next)
|
||||
await MemoryFiles.append(
|
||||
input.root,
|
||||
[
|
||||
`settings ${next.scope}`,
|
||||
input.settings.autoConsolidate === undefined ? "" : `autoConsolidate=${next.autoConsolidate}`,
|
||||
input.settings.verbose === undefined ? "" : `verbose=${next.verbose}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
)
|
||||
return { root: input.root, state: next }
|
||||
})
|
||||
}
|
||||
@@ -216,24 +204,6 @@ export namespace Memory {
|
||||
}
|
||||
const state = await MemoryFiles.readState(input.root)
|
||||
const ok = MemoryNotice.saved({ added: result.added, removed: result.removed })
|
||||
if (trigger === "explicit") {
|
||||
await MemoryFiles.decide(input.root, {
|
||||
kind: "typed",
|
||||
trigger,
|
||||
sessionID: input.sessionID,
|
||||
result: ok ? "saved" : "skipped",
|
||||
llm: false,
|
||||
parsed: true,
|
||||
fallback: false,
|
||||
tokens: input.tokens ?? 0,
|
||||
operationCount: result.operationCount,
|
||||
skippedCount: result.skipped.length || (ok ? 0 : 1),
|
||||
skipped: MemoryNotice.skip(result.skipped),
|
||||
operations: MemoryNotice.ops({ ops: accepted, skipped: result.skipped }),
|
||||
files: MemoryShared.files(accepted),
|
||||
summary: MemoryNotice.summary({ added: result.added, removed: result.removed, count: result.operationCount }),
|
||||
})
|
||||
}
|
||||
return {
|
||||
root: input.root,
|
||||
state,
|
||||
@@ -318,31 +288,6 @@ export namespace Memory {
|
||||
const hits = result?.hits ?? []
|
||||
const files = [...new Set(hits.map((hit) => hit.source))]
|
||||
const topics = [...new Set(hits.flatMap((hit) => (hit.topics?.length ? hit.topics : [hit.kind])))]
|
||||
await MemoryFiles.decide(input.root, {
|
||||
kind: "recall",
|
||||
trigger: "targeted-recall",
|
||||
sessionID: input.sessionID,
|
||||
result: result ? "recalled" : "skipped",
|
||||
llm: false,
|
||||
parsed: false,
|
||||
fallback: false,
|
||||
reason: result ? undefined : "no_matches",
|
||||
query: MemoryShared.brief(MemoryRedact.text(input.query), 240),
|
||||
topics,
|
||||
files,
|
||||
tokens: result?.tokens ?? 0,
|
||||
operationCount: hits.length,
|
||||
skippedCount: result ? 0 : 1,
|
||||
summary: result ? `targeted recall matched ${hits.length} memories` : "targeted recall found no matches",
|
||||
})
|
||||
if (result) {
|
||||
await MemoryFiles.queue(input.root, async () => {
|
||||
await MemoryFiles.append(
|
||||
input.root,
|
||||
`recall session=${input.sessionID ?? ""} hits=${result.hits.length} tokens=${result.tokens} files=${files.join(",")}`,
|
||||
)
|
||||
})
|
||||
}
|
||||
return { root: input.root, state, result, hits, files, topics }
|
||||
}
|
||||
|
||||
@@ -368,10 +313,6 @@ export namespace Memory {
|
||||
})
|
||||
await MemoryFiles.pruneSessions(input.root, state.limits.maxSessionFiles)
|
||||
const index = await MemoryIndexer.rebuild({ root: input.root, state })
|
||||
await MemoryFiles.append(
|
||||
input.root,
|
||||
`session digest session=${input.sessionID} tokens=${input.tokens ?? 0} indexTokens=${index.tokens}`,
|
||||
)
|
||||
return { root: input.root, state, skipped: false as const, index }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -226,7 +226,6 @@ export namespace MemoryIndexer {
|
||||
return MemoryFiles.queue(input.root, async () => {
|
||||
const result = await build(input)
|
||||
await MemoryFiles.writeIndex(input.root, result.text)
|
||||
await MemoryFiles.append(input.root, `regenerate index.kmem bytes=${result.bytes} tokens=${result.tokens}`)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ export namespace MemoryShared {
|
||||
]
|
||||
}
|
||||
|
||||
/** @deprecated Memory audit persistence was removed. */
|
||||
export function audit(ops: MemoryOperations.Op[]) {
|
||||
return ops.map((item) =>
|
||||
item.action === "add"
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/** Wire contract for suppressible background-capture failures: guardReason emits this as the
|
||||
* `memory.error` reason, and every client surface (TUI toast filter, VS Code provider) matches
|
||||
* against this same constant rather than a local literal. Lives here (dependency-free module)
|
||||
* so clients can import it without pulling the capture pipeline into their bundles. */
|
||||
export const TRANSIENT = "transient" as const
|
||||
|
||||
export namespace MemorySchema {
|
||||
export const VERSION = 1
|
||||
export const maxStoredDigestSummary = 4_000
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import z from "zod"
|
||||
import { MemoryFs } from "./fs"
|
||||
|
||||
/** Compatibility facade for the removed memory audit log. */
|
||||
export namespace MemoryAudit {
|
||||
const Log = z
|
||||
.object({
|
||||
kind: z.literal("log"),
|
||||
summary: z.string(),
|
||||
time: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
export type Decision =
|
||||
| {
|
||||
kind: "log"
|
||||
@@ -43,18 +33,14 @@ export namespace MemoryAudit {
|
||||
}[]
|
||||
}
|
||||
|
||||
function audit(root: string, input: Decision) {
|
||||
void root
|
||||
void input
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export async function append(root: string, text: string) {
|
||||
await audit(root, { kind: "log", result: "logged", summary: text })
|
||||
void root
|
||||
void text
|
||||
}
|
||||
|
||||
export async function decide(root: string, input: Decision) {
|
||||
await audit(root, input)
|
||||
void root
|
||||
void input
|
||||
}
|
||||
|
||||
export async function readDecisions(root: string) {
|
||||
@@ -62,24 +48,8 @@ export namespace MemoryAudit {
|
||||
return ""
|
||||
}
|
||||
|
||||
function record(input: string) {
|
||||
try {
|
||||
const data = JSON.parse(input)
|
||||
const parsed = Log.safeParse(data)
|
||||
return parsed.success ? parsed.data : undefined
|
||||
} catch (error) {
|
||||
if (MemoryFs.parse(error)) return undefined
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function readChanges(root: string) {
|
||||
const lines = (await readDecisions(root)).split("\n").flatMap((line) => {
|
||||
const data = record(line)
|
||||
if (!data) return []
|
||||
const time = data.time ?? ""
|
||||
return [`${time} ${data.summary}`.trim()]
|
||||
})
|
||||
return lines.join("\n")
|
||||
void root
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { readdir, rm } from "fs/promises"
|
||||
import path from "path"
|
||||
import { MemoryAudit } from "./audit"
|
||||
import { MemoryFs } from "./fs"
|
||||
import { MemoryMarkdown } from "./markdown"
|
||||
import { MemoryPaths } from "./paths"
|
||||
@@ -19,13 +18,10 @@ export namespace MemoryState {
|
||||
"corrections.md": "# Corrective Memory\n\n## Corrections\n",
|
||||
}
|
||||
|
||||
async function recover(root: string, file: string, error: unknown) {
|
||||
async function recover(root: string, file: string) {
|
||||
await MemoryFs.backup(file)
|
||||
const state = MemorySchema.missing()
|
||||
await writeState(root, state)
|
||||
await MemoryAudit.append(root, `recover state.json error=${MemoryFs.brief(error)}`).catch((err: unknown) =>
|
||||
MemoryFs.warn("failed to audit memory state recovery", { err, root }),
|
||||
)
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -33,14 +29,14 @@ export namespace MemoryState {
|
||||
const file = MemoryPaths.files(root).state
|
||||
const data = await MemoryFs.json(file).catch(async (error: unknown) => {
|
||||
if (MemoryFs.miss(error)) return undefined
|
||||
if (MemoryFs.parse(error)) return recover(root, file, error)
|
||||
if (MemoryFs.parse(error)) return recover(root, file)
|
||||
throw error
|
||||
})
|
||||
if (data === undefined) return MemorySchema.missing()
|
||||
return Promise.resolve()
|
||||
.then(() => MemorySchema.parse(data))
|
||||
.catch((error: unknown) => {
|
||||
if (MemoryFs.parse(error)) return recover(root, file, error)
|
||||
if (MemoryFs.parse(error)) return recover(root, file)
|
||||
throw error
|
||||
})
|
||||
}
|
||||
@@ -199,7 +195,6 @@ export namespace MemoryState {
|
||||
? { ...(await readState(root)), enabled: true, autoInject: true }
|
||||
: { ...MemorySchema.create(), enabled: true }
|
||||
await writeState(root, state)
|
||||
await MemoryAudit.append(root, "enable project source=command")
|
||||
return state
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { MemoryError, type MemoryError as Failure } from "./effect/errors"
|
||||
import { MemoryPaths } from "./effect/paths"
|
||||
import { MemoryService } from "./effect/service"
|
||||
import { MemoryRecall } from "./recall/recall"
|
||||
import { MemoryToken } from "./recall/token"
|
||||
import { MemorySchema } from "./schema"
|
||||
import recallDescription from "./prompts/tool-memory-recall.txt"
|
||||
import saveDescription from "./prompts/tool-memory-save.txt"
|
||||
@@ -115,50 +114,6 @@ export namespace MemoryTool {
|
||||
}
|
||||
}
|
||||
|
||||
function audit(
|
||||
memory: MemoryService.Interface,
|
||||
input: {
|
||||
root: string
|
||||
params: RecallParams
|
||||
current: string
|
||||
hits: MemoryRecall.Hit[]
|
||||
skipped?: string
|
||||
output: string
|
||||
},
|
||||
) {
|
||||
const files = [...new Set(input.hits.map((hit) => hit.source))]
|
||||
const topics = [...new Set(input.hits.flatMap((hit) => (hit.topics?.length ? hit.topics : [hit.kind])))]
|
||||
const query =
|
||||
input.params.query ??
|
||||
(input.params.sessionID
|
||||
? `sessionID=${input.params.sessionID}`
|
||||
: input.params.mode === "digest"
|
||||
? "recent digests"
|
||||
: undefined)
|
||||
return memory.decide({
|
||||
root: input.root,
|
||||
decision: {
|
||||
kind: "recall",
|
||||
trigger: "targeted-recall",
|
||||
sessionID: input.current,
|
||||
result: input.hits.length ? "recalled" : "skipped",
|
||||
llm: false,
|
||||
parsed: false,
|
||||
fallback: false,
|
||||
reason: input.skipped,
|
||||
query,
|
||||
topics,
|
||||
files,
|
||||
tokens: MemoryToken.estimate(input.output),
|
||||
operationCount: input.hits.length,
|
||||
skippedCount: input.hits.length ? 0 : 1,
|
||||
summary: input.hits.length
|
||||
? `memory recall returned ${input.hits.length} ${input.params.mode} hits`
|
||||
: `memory recall found no ${input.params.mode} hits`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function miss(input: { params: RecallParams; current: string }) {
|
||||
const self = input.params.sessionID === input.current
|
||||
if (self && input.params.mode === "digest") {
|
||||
@@ -239,35 +194,6 @@ export namespace MemoryTool {
|
||||
})
|
||||
}
|
||||
|
||||
function catalogAudit(
|
||||
memory: MemoryService.Interface,
|
||||
input: {
|
||||
root: string
|
||||
current: string
|
||||
query: string
|
||||
result: { output: string; count: number; files: string[] }
|
||||
},
|
||||
) {
|
||||
return memory.decide({
|
||||
root: input.root,
|
||||
decision: {
|
||||
kind: "recall",
|
||||
trigger: "targeted-recall",
|
||||
sessionID: input.current,
|
||||
result: input.result.count ? "recalled" : "skipped",
|
||||
llm: false,
|
||||
parsed: false,
|
||||
fallback: false,
|
||||
query: input.query || "all keys",
|
||||
files: input.result.files,
|
||||
tokens: MemoryToken.estimate(input.result.output),
|
||||
operationCount: input.result.count,
|
||||
skippedCount: input.result.count ? 0 : 1,
|
||||
summary: `memory catalog listed ${input.result.count} entries`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function approvalRecall(input: Recall) {
|
||||
return input.ask({
|
||||
permission: "kilo_memory_recall",
|
||||
@@ -285,7 +211,6 @@ export namespace MemoryTool {
|
||||
return Effect.gen(function* () {
|
||||
const result = yield* catalog(input.memory, { root: live.root, query })
|
||||
const safe = { ...result, output: block(result.output) }
|
||||
yield* catalogAudit(input.memory, { root: live.root, current: live.current, query, result: safe })
|
||||
yield* input.memory.recordRecall({ root: live.root, sessionID: live.current, now: Date.now(), count: result.count })
|
||||
return {
|
||||
title: `Kilo memory catalog: ${result.count} entr${result.count === 1 ? "y" : "ies"}`,
|
||||
@@ -296,22 +221,13 @@ export namespace MemoryTool {
|
||||
}
|
||||
|
||||
function recallQuery(input: Recall, live: Live) {
|
||||
return Effect.gen(function* () {
|
||||
const output = "Provide a topic query for typed/search memory recall."
|
||||
yield* audit(input.memory, {
|
||||
root: live.root,
|
||||
params: input.params,
|
||||
current: live.current,
|
||||
hits: [],
|
||||
skipped: "missing_query",
|
||||
output,
|
||||
})
|
||||
return {
|
||||
return Effect.succeed(
|
||||
{
|
||||
title: `Kilo memory ${input.params.mode}: no query`,
|
||||
output,
|
||||
output: "Provide a topic query for typed/search memory recall.",
|
||||
metadata: { sources: [], count: 0 },
|
||||
} satisfies Result
|
||||
})
|
||||
} satisfies Result,
|
||||
)
|
||||
}
|
||||
|
||||
function recallSearch(input: Recall, live: Live, query: string, mode: MemoryRecall.Mode) {
|
||||
@@ -327,15 +243,7 @@ export namespace MemoryTool {
|
||||
limit,
|
||||
})
|
||||
const hits = result?.hits ?? []
|
||||
const self = input.params.sessionID === live.current
|
||||
const skipped =
|
||||
input.params.sessionID && input.params.mode === "digest" && hits.length === 0
|
||||
? self
|
||||
? "current_session_digest"
|
||||
: "missing_session_digest"
|
||||
: undefined
|
||||
const output = hits.length ? result!.block : miss({ params: input.params, current: live.current })
|
||||
yield* audit(input.memory, { root: live.root, params: input.params, current: live.current, hits, skipped, output })
|
||||
yield* input.memory.recordRecall({ root: live.root, sessionID: live.current, now: Date.now(), count: hits.length })
|
||||
|
||||
if (hits.length === 0) {
|
||||
@@ -440,29 +348,8 @@ export namespace MemoryTool {
|
||||
return saved(input)
|
||||
}
|
||||
|
||||
function skip(input: Base & { params: SaveParams }, root: string) {
|
||||
return Effect.gen(function* () {
|
||||
const reason = input.params.reason ?? "out_of_scope"
|
||||
yield* input.memory.decide({
|
||||
root,
|
||||
decision: {
|
||||
kind: "typed",
|
||||
trigger: "explicit",
|
||||
sessionID: input.sessionID,
|
||||
result: "skipped",
|
||||
llm: false,
|
||||
parsed: true,
|
||||
fallback: false,
|
||||
reason,
|
||||
tokens: 0,
|
||||
operationCount: 0,
|
||||
skippedCount: 1,
|
||||
skipped: [{ reason }],
|
||||
summary: `explicit memory save skipped: ${reason}`,
|
||||
},
|
||||
})
|
||||
return skipped({ reason })
|
||||
})
|
||||
function skip(input: Base & { params: SaveParams }) {
|
||||
return Effect.succeed(skipped({ reason: input.params.reason ?? "out_of_scope" }))
|
||||
}
|
||||
|
||||
function forget(input: Save, root: string) {
|
||||
@@ -497,7 +384,7 @@ export namespace MemoryTool {
|
||||
const state = yield* input.memory.state({ root })
|
||||
if (!state.enabled) return disabled()
|
||||
if (input.params.action === "forget") return yield* forget(input, root)
|
||||
if (input.params.action === "skip") return yield* skip(input, root)
|
||||
if (input.params.action === "skip") return yield* skip(input)
|
||||
return yield* write(input, root)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import {
|
||||
auditOps,
|
||||
capturePlan,
|
||||
duplicateOps,
|
||||
errorReason,
|
||||
fallbackDigest,
|
||||
guardReason,
|
||||
hasSubstantialDiff,
|
||||
@@ -128,7 +128,7 @@ describe("memory capture parsing", () => {
|
||||
expect(() => salvageTyped(`{"op":"upsert_project_fact","key":"good_one","value":"Keep this fact."}`)).toThrow()
|
||||
})
|
||||
|
||||
test("redacts secrets in salvaged unsupported ops before they reach the audit", () => {
|
||||
test("redacts secrets in salvaged unsupported ops before they reach callers", () => {
|
||||
const parsed = salvageTyped(
|
||||
`{"operations":[{"op":"not_a_real_op","key":"leak","value":"key is sk-abcdefghijklmnopqrstuvwxyz"}],"skipped":[]}`,
|
||||
)
|
||||
@@ -169,17 +169,6 @@ describe("memory capture parsing", () => {
|
||||
expect(JSON.stringify(parsed.skipped)).not.toContain(secret.slice(0, 20))
|
||||
})
|
||||
|
||||
test("redacts remove audit queries before truncating", () => {
|
||||
const secret = "sk-" + "a".repeat(40)
|
||||
const query = "x".repeat(100) + secret
|
||||
const audit = auditOps([{ action: "remove", query }])
|
||||
const text = JSON.stringify(audit)
|
||||
|
||||
expect(text).toContain("[redacted]")
|
||||
expect(text).not.toContain(secret)
|
||||
expect(text).not.toContain(secret.slice(0, 20))
|
||||
})
|
||||
|
||||
test("truncates typed batches beyond the op cap instead of failing", () => {
|
||||
const ops = Array.from(
|
||||
{ length: 20 },
|
||||
@@ -332,6 +321,11 @@ describe("memory capture parsing", () => {
|
||||
input: base,
|
||||
expected: { session: true, digestDue: true, typedCall: true, typedWork: true, skipReason: undefined },
|
||||
},
|
||||
{
|
||||
name: "expected work: a persisted null typed clock does not throttle capture",
|
||||
input: { ...base, lastTypedConsolidationAt: null },
|
||||
expected: { session: true, digestDue: true, typedCall: true, typedWork: true, skipReason: undefined },
|
||||
},
|
||||
{
|
||||
name: "expected idle flush: completed turn inside interval skips now",
|
||||
input: { ...base, priorTime: 900, lastTypedConsolidationAt: 900 },
|
||||
@@ -650,6 +644,70 @@ describe("memory capture parsing", () => {
|
||||
)
|
||||
expect(guardReason("429 too many requests")).toBe("rate_limit_guard")
|
||||
expect(guardReason("billing credits exhausted")).toBe("quota_guard")
|
||||
expect(guardReason("request timed out")).toBe("transient")
|
||||
expect(guardReason("deadline exceeded")).toBe("transient")
|
||||
expect(guardReason("DeadlineExceeded")).toBe("transient")
|
||||
expect(guardReason('cause={"code":"ETIMEDOUT"}')).toBe("transient")
|
||||
expect(guardReason("504 Gateway Timeout")).toBe("transient")
|
||||
expect(guardReason("Connect Timeout Error")).toBe("transient")
|
||||
expect(guardReason("Headers Timeout Error")).toBe("transient")
|
||||
expect(guardReason("Body Timeout Error")).toBe("transient")
|
||||
expect(guardReason("connect_timeout")).toBe("transient")
|
||||
expect(guardReason('status=400 body={"error":"invalid parameter: timeout"}')).toBeUndefined()
|
||||
expect(guardReason("set request timeout to 30000")).toBeUndefined()
|
||||
const timeout = new Error("request aborted")
|
||||
timeout.name = "TimeoutError"
|
||||
expect(guardReason(timeout)).toBe("transient")
|
||||
expect(errorReason(timeout)).toBe("request aborted")
|
||||
expect(guardReason(Object.assign(new Error("request failed"), { status: 504 }))).toBe("transient")
|
||||
expect(
|
||||
guardReason(Object.assign(new Error("request failed"), { cause: { code: "UND_ERR_CONNECT_TIMEOUT" } })),
|
||||
).toBe("transient")
|
||||
expect(
|
||||
guardReason(Object.assign(new Error("request failed"), { cause: { code: "UND_ERR_HEADERS_TIMEOUT" } })),
|
||||
).toBe("transient")
|
||||
expect(guardReason(Object.assign(new Error("request failed"), { cause: { code: "ETIMEDOUT" } }))).toBe(
|
||||
"transient",
|
||||
)
|
||||
expect(
|
||||
guardReason(Object.assign(new Error("failed after 2 attempts"), { errors: [{ statusCode: 504 }] })),
|
||||
).toBe("transient")
|
||||
expect(guardReason(new Error("set request timeout to 30000"))).toBeUndefined()
|
||||
expect(guardReason(new Error("request timed out"))).toBe("transient")
|
||||
expect(guardReason(Object.assign(new Error("request failed"), { cause: new Error("connect timeout") }))).toBe(
|
||||
"transient",
|
||||
)
|
||||
expect(
|
||||
guardReason(
|
||||
Object.assign(new Error("failed after 2 attempts"), {
|
||||
errors: [Object.assign(new Error("rate limit reached"), { status: 429 }), timeout],
|
||||
}),
|
||||
),
|
||||
).toBe("rate_limit_guard")
|
||||
expect(
|
||||
guardReason(
|
||||
Object.assign(new Error("failed after 2 attempts"), {
|
||||
errors: [new Error("insufficient quota available"), timeout],
|
||||
}),
|
||||
),
|
||||
).toBe("quota_guard")
|
||||
expect(
|
||||
guardReason(
|
||||
Object.assign(new Error("request failed"), {
|
||||
cause: Object.assign(new Error("rate limit reached"), { status: 429 }),
|
||||
}),
|
||||
),
|
||||
).toBe("rate_limit_guard")
|
||||
expect(
|
||||
guardReason(
|
||||
Object.assign(new Error("timed out"), {
|
||||
name: "TimeoutError",
|
||||
cause: new Error("billing credit balance exhausted"),
|
||||
}),
|
||||
),
|
||||
).toBe("quota_guard")
|
||||
expect(guardReason("timeout after 429 too many requests")).toBe("rate_limit_guard")
|
||||
expect(guardReason("timeout after quota exceeded")).toBe("quota_guard")
|
||||
})
|
||||
|
||||
test("redacts common secret token shapes", () => {
|
||||
|
||||
@@ -50,6 +50,22 @@ describe("memory core package", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("legacy audit compatibility APIs remain no-op", async () => {
|
||||
await use(async (t) => {
|
||||
await Memory.enable({ root: t.root })
|
||||
await MemoryFiles.append(t.root, "provider error with sensitive detail")
|
||||
await MemoryFiles.decide(t.root, {
|
||||
kind: "typed",
|
||||
result: "error",
|
||||
reason: "provider error with sensitive detail",
|
||||
})
|
||||
|
||||
expect(await MemoryFiles.readChanges(t.root)).toBe("")
|
||||
expect(await MemoryFiles.readDecisions(t.root)).toBe("")
|
||||
expect(await Bun.file(path.join(t.root, "decisions.jsonl")).exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
test("prepare removes legacy decisions once from owned memory roots", async () => {
|
||||
await use(async (t) => {
|
||||
await Memory.enable({ root: t.root })
|
||||
|
||||
@@ -7,6 +7,7 @@ import { digestPrompt, typedPrompt } from "../src/capture/capture"
|
||||
import { MemoryCapture } from "../src/effect/capture"
|
||||
import { MemoryEvents } from "../src/effect/events"
|
||||
import { KiloMemory } from "../src/effect/index"
|
||||
import { MemoryLog } from "../src/effect/log"
|
||||
import type { MemoryPorts } from "../src/effect/ports"
|
||||
import { MemoryService } from "../src/effect/service"
|
||||
import { MemoryTimers } from "../src/effect/timers"
|
||||
@@ -49,11 +50,18 @@ function session(turn: MemoryPorts.TurnView | undefined): MemoryPorts.SessionPor
|
||||
|
||||
/** Model port that answers digest/typed calls from canned JSON, keyed by system prompt so it is
|
||||
* order-independent (digest and typed run concurrently). */
|
||||
function model(input: { digest: string; typed: string; fallback?: string; onRun?: (system: string) => void }): MemoryPorts.ModelPort {
|
||||
function model(input: {
|
||||
digest: string
|
||||
typed: string
|
||||
fallback?: string
|
||||
fail?: Error
|
||||
onRun?: (system: string) => void
|
||||
}): MemoryPorts.ModelPort {
|
||||
return {
|
||||
resolve: () => Effect.succeed({ handle: {}, ...(input.fallback ? { fallback: { reason: input.fallback } } : {}) }),
|
||||
run: async ({ system }) => {
|
||||
input.onRun?.(system)
|
||||
if (system === typedPrompt && input.fail) throw input.fail
|
||||
const text = system === digestPrompt ? input.digest : system === typedPrompt ? input.typed : "{}"
|
||||
return { text, usage: USAGE }
|
||||
},
|
||||
@@ -66,6 +74,7 @@ function run(input: {
|
||||
model: MemoryPorts.ModelPort
|
||||
memoryModel?: string
|
||||
reason?: "completed" | "interrupted" | "error"
|
||||
bypassInterval?: boolean
|
||||
}) {
|
||||
return Effect.runPromise(
|
||||
MemoryCapture.turn({
|
||||
@@ -75,6 +84,7 @@ function run(input: {
|
||||
model: input.model,
|
||||
memoryModel: input.memoryModel,
|
||||
reason: input.reason ?? "completed",
|
||||
bypassInterval: input.bypassInterval,
|
||||
}).pipe(Effect.provideService(MemoryService.Service, MemoryService.make())),
|
||||
)
|
||||
}
|
||||
@@ -107,6 +117,121 @@ describe("MemoryCapture (fake ports)", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("typed timeout preserves digest progress without advancing the typed clock", async () => {
|
||||
const t = await tmp()
|
||||
const events: MemoryEvents.Status[] = []
|
||||
const logs: string[] = []
|
||||
try {
|
||||
await KiloMemory.enable({ root: t.root })
|
||||
await KiloMemory.configure({ root: t.root, settings: { autoConsolidate: true } })
|
||||
MemoryLog.setWarn((message, meta) => logs.push(`${message}:${meta?.reason}:${meta?.detail}`))
|
||||
MemoryEvents.setSink((input) => {
|
||||
events.push(input.payload)
|
||||
})
|
||||
|
||||
const result = await run({
|
||||
root: t.root,
|
||||
session: session(view()),
|
||||
model: model({
|
||||
digest: '{"topic":"repo setup","summary":"Explored repo setup commands. Next step: verify memory tests."}',
|
||||
typed: "{}",
|
||||
fail: new DOMException("memory model timed out", "TimeoutError"),
|
||||
}),
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({ skipped: false, operationCount: 0 })
|
||||
const state = await MemoryFiles.readState(t.root)
|
||||
expect(state.stats.lastTypedConsolidationAt).toBeNull()
|
||||
expect(state.stats.lastSessionSavedAt).toEqual(expect.any(Number))
|
||||
expect(state.stats.lastConsolidatedMessageID).toBe("msg_assistant")
|
||||
expect(events.find((item) => item.state === "error")?.reason).toBe("transient")
|
||||
expect(logs).toEqual(["memory capture transient failure:transient:memory model timed out"])
|
||||
|
||||
const calls: string[] = []
|
||||
const retry = await run({
|
||||
root: t.root,
|
||||
session: session(view()),
|
||||
model: model({
|
||||
digest: "{}",
|
||||
typed: '{"operations":[],"skipped":[]}',
|
||||
onRun: (system) => calls.push(system),
|
||||
}),
|
||||
bypassInterval: true,
|
||||
})
|
||||
expect(retry).toMatchObject({ skipped: true, reason: "no_new_content" })
|
||||
expect(calls).toEqual([])
|
||||
|
||||
const next = await run({
|
||||
root: t.root,
|
||||
session: session(
|
||||
view({
|
||||
assistant: "Use bun install, then run the package tests and typecheck.",
|
||||
lastAssistantID: "msg_assistant_next",
|
||||
}),
|
||||
),
|
||||
model: model({
|
||||
digest: "{}",
|
||||
typed: '{"operations":[],"skipped":[]}',
|
||||
onRun: (system) => calls.push(system),
|
||||
}),
|
||||
})
|
||||
expect(next).toMatchObject({ skipped: false, operationCount: 0 })
|
||||
expect(calls).toEqual([typedPrompt])
|
||||
const updated = await MemoryFiles.readState(t.root)
|
||||
expect(updated.stats.lastTypedConsolidationAt).toEqual(expect.any(Number))
|
||||
expect(updated.stats.lastConsolidatedMessageID).toBe("msg_assistant_next")
|
||||
} finally {
|
||||
MemoryLog.setWarn(() => {})
|
||||
MemoryEvents.setSink(() => {})
|
||||
await t.done()
|
||||
}
|
||||
})
|
||||
|
||||
test("fallback commit preserves metrics from the prior successful typed consolidation", async () => {
|
||||
const t = await tmp()
|
||||
try {
|
||||
await KiloMemory.enable({ root: t.root })
|
||||
await KiloMemory.configure({ root: t.root, settings: { autoConsolidate: true } })
|
||||
|
||||
const first = await run({
|
||||
root: t.root,
|
||||
session: session(view()),
|
||||
model: model({
|
||||
digest: '{"topic":"repo setup","summary":"Explored repo setup."}',
|
||||
typed:
|
||||
'{"operations":[{"op":"upsert_environment_fact","section":"Commands","key":"test_cmd","value":"bun test"}],"skipped":[]}',
|
||||
}),
|
||||
})
|
||||
expect(first).toMatchObject({ skipped: false, operationCount: 1 })
|
||||
const initial = await MemoryFiles.readState(t.root)
|
||||
expect(initial.stats.lastOperationCount).toBe(1)
|
||||
expect(initial.stats.lastTypedConsolidationAt).toEqual(expect.any(Number))
|
||||
|
||||
const second = await run({
|
||||
root: t.root,
|
||||
session: session(
|
||||
view({
|
||||
assistant: "Investigated a timeout edge case.",
|
||||
lastAssistantID: "msg_assistant_timeout",
|
||||
}),
|
||||
),
|
||||
model: model({
|
||||
digest: '{"topic":"investigation","summary":"Investigated timeouts."}',
|
||||
typed: "{}",
|
||||
fail: new DOMException("memory model timed out", "TimeoutError"),
|
||||
}),
|
||||
bypassInterval: true,
|
||||
})
|
||||
expect(second).toMatchObject({ skipped: false, operationCount: 0 })
|
||||
const preserved = await MemoryFiles.readState(t.root)
|
||||
expect(preserved.stats.lastOperationCount).toBe(1)
|
||||
expect(preserved.stats.lastTypedConsolidationAt).toBe(initial.stats.lastTypedConsolidationAt)
|
||||
expect(preserved.stats.lastConsolidatedMessageID).toBe("msg_assistant_timeout")
|
||||
} finally {
|
||||
await t.done()
|
||||
}
|
||||
})
|
||||
|
||||
test("turn-close skips a secret-like op and applies the rest of the batch", async () => {
|
||||
const t = await tmp()
|
||||
const events: MemoryEvents.Status[] = []
|
||||
|
||||
@@ -12,6 +12,24 @@
|
||||
border: 1px solid var(--border-weak-base);
|
||||
}
|
||||
|
||||
&[data-size="large"] [data-slot="dialog-container"] {
|
||||
width: min(calc(100vw - 32px), 800px);
|
||||
height: min(calc(100vh - 32px), 720px);
|
||||
}
|
||||
|
||||
&[data-size="large"] [data-slot="dialog-content"] {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
&[data-size="x-large"] [data-slot="dialog-container"] {
|
||||
width: min(calc(100vw - 32px), 980px);
|
||||
height: min(calc(100vh - 32px), 800px);
|
||||
}
|
||||
|
||||
&[data-size="x-large"] [data-slot="dialog-content"] {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
[data-slot="dialog-header"] {
|
||||
font-size: var(--kilo-font-size-14);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,18 @@ import { Icon as Upstream, type IconProps as Props } from "@opencode-ai/ui/icon"
|
||||
import { splitProps } from "solid-js"
|
||||
|
||||
const icons: Record<string, { path: string; viewBox: string }> = {
|
||||
"circle-x-outline": {
|
||||
viewBox: "0 0 20 20",
|
||||
path: `<path d="M7.5 7.5L12.5 12.5M12.5 7.5L7.5 12.5M18.3333 10C18.3333 14.6024 14.6024 18.3333 10 18.3333C5.39763 18.3333 1.66667 14.6024 1.66667 10C1.66667 5.39763 5.39763 1.66667 10 1.66667C14.6024 1.66667 18.3333 5.39763 18.3333 10Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
},
|
||||
"pull-request": {
|
||||
viewBox: "0 0 20 20",
|
||||
path: `<path d="M1.875 4.0625a2.8125 2.8125 0 1 1 3.75 2.6525v6.57a2.8138 2.8138 0 1 1-1.875 0V6.715A2.8125 2.8125 0 0 1 1.875 4.0625Zm7.096-.22125L11.96625.84625A.3125.3125 0 0 1 12.5 1.0675V3.125h1.25A3.125 3.125 0 0 1 16.875 6.25v7.035a2.8138 2.8138 0 1 1-1.875 0V6.25a1.25 1.25 0 0 0-1.25-1.25H12.5v2.0575a.3125.3125 0 0 1-.53375.22125L8.97125 4.34125a.3125.3125 0 0 1 0-.4425ZM4.6875 3.125a.9375.9375 0 1 0 0 1.875.9375.9375 0 0 0 0-1.875Zm0 11.875a.9375.9375 0 1 0 0 1.875.9375.9375 0 0 0 0-1.875Zm10.3125.9375a.9375.9375 0 1 0 1.875 0 .9375.9375 0 0 0-1.875 0Z" fill="currentColor"/>`,
|
||||
},
|
||||
refresh: {
|
||||
viewBox: "0 0 20 20",
|
||||
path: `<path d="M17.0837 10.0003C17.0837 13.9123 13.9123 17.0837 10.0003 17.0837C6.08833 17.0837 2.91699 13.9123 2.91699 10.0003C2.91699 6.08833 6.08833 2.91699 10.0003 2.91699C12.3717 2.91699 14.4722 4.07428 15.7698 5.83366M15.7698 5.83366V2.91699M15.7698 5.83366H12.8532" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
},
|
||||
memory: {
|
||||
viewBox: "0 0 24 24",
|
||||
path: `<path d="M2 7h4M2 12h4M2 17h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><rect width="16" height="20" x="4" y="2" rx="2" stroke="currentColor" stroke-width="2"/>`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as path from "path"
|
||||
import { existsSync } from "fs"
|
||||
import * as vscode from "vscode"
|
||||
import { TRANSIENT as MEMORY_TRANSIENT } from "@kilocode/kilo-memory/schema"
|
||||
import type {
|
||||
KiloClient,
|
||||
Session,
|
||||
@@ -4307,8 +4308,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
|
||||
for (const sid of trackedByDir) targets.add(sid)
|
||||
if (local && active) targets.add(active)
|
||||
if (targets.size === 0 && local) targets.add(undefined)
|
||||
const detail =
|
||||
props.detail && typeof props.detail === "object"
|
||||
const transient = event.type === "memory.error" && props.reason === MEMORY_TRANSIENT
|
||||
const detail = transient
|
||||
? undefined
|
||||
: props.detail && typeof props.detail === "object"
|
||||
? props.detail
|
||||
: event.type === "memory.error" && typeof props.reason === "string"
|
||||
? { type: "error", message: props.reason, reason: props.reason }
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { ExecFileOptionsWithStringEncoding } from "child_process"
|
||||
import type { Worktree } from "./WorktreeStateManager"
|
||||
import type { PRStatus, PRCheck, PRComment, CheckStatus, AggregateCheckStatus, PRState, ReviewDecision } from "./types"
|
||||
import type { PRStatus, PRCheck, PRComment, PRReviewer, AggregateCheckStatus } from "./types"
|
||||
import { execWithShellEnv } from "./shell-env"
|
||||
import { execGhRead } from "./gh"
|
||||
import { classifyPRError } from "./git-import"
|
||||
import type { Semaphore } from "./semaphore"
|
||||
import { parsePRResult, checkStatus, formatCheckDuration, parseComments, parseReviewers } from "./am-pr-utils"
|
||||
import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./am-pr-types"
|
||||
|
||||
interface PRStatusPollerOptions {
|
||||
getWorktrees: () => Worktree[]
|
||||
@@ -17,6 +19,7 @@ interface PRStatusPollerOptions {
|
||||
}
|
||||
|
||||
const GH_PROBE_TTL = 300_000 // 5 minutes — gh installation state rarely changes at runtime
|
||||
const GH_PROBE_FAILURE_TTL = 30_000 // 30 seconds — retry faster after a failed probe
|
||||
const MAX_BACKOFF = 120_000 // 2 minutes — cap for exponential backoff on repeated errors
|
||||
const BACKOFF_MULTIPLIER = 2
|
||||
const PR_LOOKUP_TTL = 10_000 // 10 seconds — short TTL; only the active worktree polls so this stays cheap
|
||||
@@ -171,7 +174,8 @@ export class PRStatusPoller {
|
||||
|
||||
private async probeGh(): Promise<boolean> {
|
||||
const now = Date.now()
|
||||
if (this.ghAvailable !== undefined && now - this.ghProbeTime < GH_PROBE_TTL) {
|
||||
const ttl = this.ghAvailable === false ? GH_PROBE_FAILURE_TTL : GH_PROBE_TTL
|
||||
if (this.ghAvailable !== undefined && now - this.ghProbeTime < ttl) {
|
||||
return this.ghAvailable
|
||||
}
|
||||
try {
|
||||
@@ -244,8 +248,9 @@ export class PRStatusPoller {
|
||||
return
|
||||
}
|
||||
|
||||
const [checks, comments] = await Promise.all([
|
||||
const [checks, reviewers, comments] = await Promise.all([
|
||||
this.fetchChecks(pr.number, wt.path),
|
||||
this.fetchReviewers(pr.number, wt.path),
|
||||
this.activeWorktreeId === worktreeId ? this.fetchComments(pr.number, wt.path) : undefined,
|
||||
])
|
||||
if (this.stale(generation)) return
|
||||
@@ -253,17 +258,22 @@ export class PRStatusPoller {
|
||||
const status: PRStatus = {
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
body: pr.body,
|
||||
url: pr.url,
|
||||
state: pr.state,
|
||||
review: pr.review,
|
||||
checks,
|
||||
...(comments && { comments }),
|
||||
reviewers,
|
||||
...(comments && {
|
||||
comments: { total: comments.total, unresolved: comments.unresolved, comments: comments.comments },
|
||||
}),
|
||||
additions: pr.additions,
|
||||
deletions: pr.deletions,
|
||||
files: pr.files,
|
||||
}
|
||||
|
||||
const hash = `${worktreeId}:${pr.number}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}`
|
||||
const reviewersSig = reviewers.map((r) => `${r.login}:${r.state}`).join(",")
|
||||
const hash = `${worktreeId}:${pr.number}:${pr.title}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${reviewersSig}:${pr.body ?? ""}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}`
|
||||
if (this.lastHash.get(worktreeId) === hash) return
|
||||
this.lastHash.set(worktreeId, hash)
|
||||
|
||||
@@ -292,7 +302,7 @@ export class PRStatusPoller {
|
||||
}
|
||||
|
||||
private static readonly PR_JSON_FIELDS =
|
||||
"number,title,url,state,isDraft,reviewDecision,additions,deletions,changedFiles,headRefName,headRefOid"
|
||||
"number,title,body,url,state,isDraft,reviewDecision,additions,deletions,changedFiles,headRefName,headRefOid"
|
||||
|
||||
/** Return a cached PR lookup if still fresh, otherwise fetch and cache.
|
||||
* Keyed by branch name so multiple worktrees on the same branch share
|
||||
@@ -374,7 +384,7 @@ export class PRStatusPoller {
|
||||
passed: number
|
||||
failed: number
|
||||
pending: number
|
||||
items: PRCheck[]
|
||||
checks: PRCheck[]
|
||||
}> {
|
||||
try {
|
||||
const { stdout } = await this.gh(
|
||||
@@ -389,24 +399,24 @@ export class PRStatusPoller {
|
||||
completedAt?: string
|
||||
}>
|
||||
|
||||
const items: PRCheck[] = data.map((c) => ({
|
||||
const checks: PRCheck[] = data.map((c) => ({
|
||||
name: c.name,
|
||||
status: mapCheckStatus(c.state),
|
||||
status: checkStatus(c.state),
|
||||
url: c.link,
|
||||
duration: formatCheckDuration(c.startedAt, c.completedAt),
|
||||
}))
|
||||
|
||||
const total = items.length
|
||||
const passed = items.filter((c) => c.status === "success").length
|
||||
const failed = items.filter((c) => c.status === "failure").length
|
||||
const pending = items.filter((c) => c.status === "pending").length
|
||||
const total = checks.filter((c) => c.status !== "skipped").length
|
||||
const passed = checks.filter((c) => c.status === "success").length
|
||||
const failed = checks.filter((c) => c.status === "failure").length
|
||||
const pending = checks.filter((c) => c.status === "pending").length
|
||||
|
||||
const status: AggregateCheckStatus =
|
||||
total === 0 ? "none" : failed > 0 ? "failure" : pending > 0 ? "pending" : "success"
|
||||
|
||||
return { status, total, passed, failed, pending, items }
|
||||
return { status, total, passed, failed, pending, checks }
|
||||
} catch {
|
||||
return { status: "none", total: 0, passed: 0, failed: 0, pending: 0, items: [] }
|
||||
return { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,13 +434,53 @@ export class PRStatusPoller {
|
||||
return info
|
||||
}
|
||||
|
||||
private async fetchReviewers(prNumber: number, cwd: string): Promise<PRReviewer[]> {
|
||||
try {
|
||||
const repo = await this.getRepoInfo(cwd)
|
||||
const query = `query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
reviewRequests(first: 20) {
|
||||
nodes { requestedReviewer { ... on User { login avatarUrl } } }
|
||||
}
|
||||
reviews(last: 20, states: [APPROVED, CHANGES_REQUESTED, COMMENTED]) {
|
||||
nodes { author { login avatarUrl } state }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
const { stdout } = await this.gh(
|
||||
[
|
||||
"api",
|
||||
"graphql",
|
||||
"-f",
|
||||
`query=${query}`,
|
||||
"-F",
|
||||
`owner=${repo.owner}`,
|
||||
"-F",
|
||||
`repo=${repo.name}`,
|
||||
"-F",
|
||||
`number=${prNumber}`,
|
||||
],
|
||||
{ cwd, timeout: 15_000 },
|
||||
)
|
||||
const pr = JSON.parse(stdout)?.data?.repository?.pullRequest
|
||||
return parseReviewers(
|
||||
(pr?.reviewRequests?.nodes ?? []) as GhReviewRequest[],
|
||||
(pr?.reviews?.nodes ?? []) as GhReview[],
|
||||
)
|
||||
} catch (err) {
|
||||
this.options.log("Failed to fetch PR reviewers:", err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchComments(
|
||||
prNumber: number,
|
||||
cwd: string,
|
||||
): Promise<{ total: number; unresolved: number; items: PRComment[] }> {
|
||||
): Promise<{ total: number; unresolved: number; comments: PRComment[] }> {
|
||||
try {
|
||||
const repo = await this.getRepoInfo(cwd)
|
||||
|
||||
const query = `query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
@@ -469,107 +519,16 @@ export class PRStatusPoller {
|
||||
],
|
||||
{ cwd, timeout: 15_000 },
|
||||
)
|
||||
const result = JSON.parse(stdout)
|
||||
const threads = result?.data?.repository?.pullRequest?.reviewThreads?.nodes ?? []
|
||||
|
||||
const items: PRComment[] = []
|
||||
for (const thread of threads) {
|
||||
const first = thread.comments?.nodes?.[0]
|
||||
if (!first) continue
|
||||
items.push({
|
||||
id: first.id,
|
||||
author: first.author?.login ?? "unknown",
|
||||
avatar: first.author?.avatarUrl,
|
||||
body: first.body ?? "",
|
||||
file: first.path,
|
||||
line: first.line,
|
||||
url: first.url,
|
||||
resolved: thread.isResolved ?? false,
|
||||
createdAt: first.createdAt ? new Date(first.createdAt).getTime() : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const total = items.length
|
||||
const unresolved = items.filter((c) => !c.resolved).length
|
||||
return { total, unresolved, items }
|
||||
const pr = JSON.parse(stdout)?.data?.repository?.pullRequest
|
||||
const comments = parseComments((pr?.reviewThreads?.nodes ?? []) as GhThread[])
|
||||
return { total: comments.length, unresolved: comments.filter((c) => !c.resolved).length, comments }
|
||||
} catch (err) {
|
||||
this.options.log("Failed to fetch PR comments:", err)
|
||||
return { total: 0, unresolved: 0, items: [] }
|
||||
return { total: 0, unresolved: 0, comments: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PRResult {
|
||||
number: number
|
||||
title: string
|
||||
url: string
|
||||
state: PRState
|
||||
review: ReviewDecision | null
|
||||
additions: number
|
||||
deletions: number
|
||||
files: number
|
||||
}
|
||||
|
||||
function parsePRResult(json: string): PRResult | null {
|
||||
const data = JSON.parse(json)
|
||||
if (!data.number) return null
|
||||
return {
|
||||
number: data.number,
|
||||
title: data.title ?? "",
|
||||
url: data.url ?? "",
|
||||
state: parsePRState(data.isDraft, data.state),
|
||||
review: parseReviewDecision(data.reviewDecision),
|
||||
additions: data.additions ?? 0,
|
||||
deletions: data.deletions ?? 0,
|
||||
files: data.changedFiles ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
function parsePRState(isDraft: boolean, ghState: string): PRState {
|
||||
if (isDraft) return "draft"
|
||||
if (ghState === "MERGED") return "merged"
|
||||
if (ghState === "CLOSED") return "closed"
|
||||
return "open"
|
||||
}
|
||||
|
||||
function parseReviewDecision(decision: string | undefined): ReviewDecision | null {
|
||||
if (decision === "APPROVED") return "approved"
|
||||
if (decision === "CHANGES_REQUESTED") return "changes_requested"
|
||||
if (decision === "REVIEW_REQUIRED") return "pending"
|
||||
return null
|
||||
}
|
||||
|
||||
function mapCheckStatus(state: string): CheckStatus {
|
||||
switch (state.toUpperCase()) {
|
||||
case "SUCCESS":
|
||||
return "success"
|
||||
case "FAILURE":
|
||||
case "ERROR":
|
||||
return "failure"
|
||||
case "PENDING":
|
||||
case "QUEUED":
|
||||
case "IN_PROGRESS":
|
||||
case "REQUESTED":
|
||||
case "WAITING":
|
||||
return "pending"
|
||||
case "SKIPPED":
|
||||
return "skipped"
|
||||
case "CANCELLED":
|
||||
case "TIMED_OUT":
|
||||
case "STALE":
|
||||
case "STARTUP_FAILURE":
|
||||
return "cancelled"
|
||||
default:
|
||||
return "pending"
|
||||
}
|
||||
}
|
||||
|
||||
function formatCheckDuration(startedAt?: string, completedAt?: string): string | undefined {
|
||||
if (!startedAt || !completedAt) return undefined
|
||||
const secs = Math.round((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 1000)
|
||||
return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s`
|
||||
}
|
||||
|
||||
/** Run async thunks with bounded concurrency, returning settled results. */
|
||||
async function settled<T>(thunks: (() => Promise<T>)[], concurrency: number): Promise<PromiseSettledResult<T>[]> {
|
||||
const results: PromiseSettledResult<T>[] = new Array(thunks.length)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { PRState, ReviewDecision } from "./types"
|
||||
|
||||
// Raw shapes returned by `gh pr view --json`
|
||||
|
||||
export interface GhAuthor {
|
||||
login?: string
|
||||
avatarUrl?: string
|
||||
}
|
||||
export interface GhComment {
|
||||
id: string
|
||||
author?: GhAuthor
|
||||
body?: string
|
||||
path?: string
|
||||
line?: number
|
||||
url?: string
|
||||
createdAt?: string
|
||||
}
|
||||
export interface GhThread {
|
||||
isResolved?: boolean
|
||||
comments?: { nodes?: GhComment[] }
|
||||
}
|
||||
export interface GhReviewRequest {
|
||||
requestedReviewer?: GhAuthor
|
||||
}
|
||||
export interface GhReview {
|
||||
author?: GhAuthor
|
||||
state?: string
|
||||
}
|
||||
|
||||
export interface PRResult {
|
||||
number: number
|
||||
title: string
|
||||
body: string
|
||||
url: string
|
||||
state: PRState
|
||||
review: ReviewDecision | null
|
||||
additions: number
|
||||
deletions: number
|
||||
files: number
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { CheckStatus, PRComment, PRReviewer, ReviewerState } from "./types"
|
||||
import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./am-pr-types"
|
||||
|
||||
export function parsePRResult(json: string): PRResult | null {
|
||||
const data = JSON.parse(json)
|
||||
if (!data.number) return null
|
||||
const state = data.isDraft ? "draft" : (data.state?.toLowerCase() ?? "open")
|
||||
const decision = data.reviewDecision as string | undefined
|
||||
const review =
|
||||
decision === "APPROVED"
|
||||
? "approved"
|
||||
: decision === "CHANGES_REQUESTED"
|
||||
? "changes_requested"
|
||||
: decision === "REVIEW_REQUIRED"
|
||||
? "pending"
|
||||
: null
|
||||
return {
|
||||
number: data.number,
|
||||
title: data.title ?? "",
|
||||
body: data.body ?? "",
|
||||
url: data.url ?? "",
|
||||
state,
|
||||
review,
|
||||
additions: data.additions ?? 0,
|
||||
deletions: data.deletions ?? 0,
|
||||
files: data.changedFiles ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function checkStatus(state: string): CheckStatus {
|
||||
switch (state.toUpperCase()) {
|
||||
case "SUCCESS":
|
||||
case "NEUTRAL":
|
||||
return "success"
|
||||
case "FAILURE":
|
||||
case "ERROR":
|
||||
case "ACTION_REQUIRED":
|
||||
return "failure"
|
||||
case "PENDING":
|
||||
case "QUEUED":
|
||||
case "IN_PROGRESS":
|
||||
case "REQUESTED":
|
||||
case "WAITING":
|
||||
return "pending"
|
||||
case "SKIPPED":
|
||||
return "skipped"
|
||||
case "CANCELLED":
|
||||
case "TIMED_OUT":
|
||||
case "STALE":
|
||||
case "STARTUP_FAILURE":
|
||||
return "cancelled"
|
||||
default:
|
||||
return "pending"
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCheckDuration(startedAt?: string, completedAt?: string): string | undefined {
|
||||
if (!startedAt || !completedAt) return undefined
|
||||
const secs = Math.round((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 1000)
|
||||
return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s`
|
||||
}
|
||||
|
||||
const REVIEWER_STATE: Record<string, ReviewerState> = {
|
||||
APPROVED: "approved",
|
||||
CHANGES_REQUESTED: "changes_requested",
|
||||
COMMENTED: "commented",
|
||||
}
|
||||
|
||||
export function parseComments(threads: GhThread[]): PRComment[] {
|
||||
const items: PRComment[] = []
|
||||
for (const thread of threads) {
|
||||
const first = thread.comments?.nodes?.[0]
|
||||
if (!first) continue
|
||||
items.push({
|
||||
id: first.id,
|
||||
author: first.author?.login ?? "unknown",
|
||||
avatar: first.author?.avatarUrl,
|
||||
body: first.body ?? "",
|
||||
file: first.path,
|
||||
line: first.line,
|
||||
url: first.url,
|
||||
resolved: thread.isResolved ?? false,
|
||||
createdAt: first.createdAt ? new Date(first.createdAt).getTime() : undefined,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
export function parseReviewers(requests: GhReviewRequest[], reviews: GhReview[]): PRReviewer[] {
|
||||
const map = new Map<string, PRReviewer>()
|
||||
for (const node of requests) {
|
||||
const user = node.requestedReviewer
|
||||
if (!user?.login) continue
|
||||
map.set(user.login, { login: user.login, avatar: user.avatarUrl, state: "pending" })
|
||||
}
|
||||
for (const node of reviews) {
|
||||
const login = node.author?.login
|
||||
if (!login) continue
|
||||
const state = REVIEWER_STATE[node.state ?? ""] ?? "pending"
|
||||
if (!map.has(login) || state !== "commented") {
|
||||
map.set(login, { login, avatar: node.author?.avatarUrl, state })
|
||||
}
|
||||
}
|
||||
return [...map.values()]
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export class PRStatusBridge {
|
||||
readonly poller: PRStatusPoller
|
||||
private readonly cache = new Map<string, AgentManagerOutMessage>()
|
||||
private readonly host: PRBridgeHost
|
||||
private lastErrorNotified: "gh_missing" | "gh_auth" | "fetch_failed" | undefined
|
||||
|
||||
constructor(host: PRBridgeHost) {
|
||||
this.host = host
|
||||
@@ -61,6 +62,8 @@ export class PRStatusBridge {
|
||||
/** Replay cached PR statuses to a freshly-connected webview. */
|
||||
replay(): void {
|
||||
this.cache.forEach((msg) => this.host.postToWebview(msg))
|
||||
if (this.lastErrorNotified === "gh_auth" || this.lastErrorNotified === "gh_missing")
|
||||
this.host.postToWebview({ type: "agentManager.prError", error: this.lastErrorNotified } as AgentManagerOutMessage)
|
||||
}
|
||||
|
||||
snapshot(): Map<string, PRStatus> {
|
||||
@@ -78,8 +81,8 @@ export class PRStatusBridge {
|
||||
return true
|
||||
}
|
||||
if (m.type === "agentManager.openPR") {
|
||||
const wt = this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId)
|
||||
if (wt?.prUrl) this.host.openExternal(wt.prUrl)
|
||||
const url = (m.url as string) ?? this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId)?.prUrl
|
||||
if (url) this.host.openExternal(url)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -93,6 +96,13 @@ export class PRStatusBridge {
|
||||
reset(): void {
|
||||
this.poller.stop()
|
||||
this.cache.clear()
|
||||
this.lastErrorNotified = undefined
|
||||
}
|
||||
|
||||
notifyError(err: "gh_missing" | "gh_auth" | "fetch_failed"): void {
|
||||
if (this.lastErrorNotified === err) return
|
||||
this.lastErrorNotified = err
|
||||
this.host.postToWebview({ type: "agentManager.prError", error: err } as AgentManagerOutMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +125,15 @@ function bridgePollerOpts(bridge: PRStatusBridge, host: PRBridgeHost) {
|
||||
pr: null,
|
||||
error: err,
|
||||
} as AgentManagerOutMessage)
|
||||
// Always forward auth/missing errors so the webview can show a toast,
|
||||
// regardless of whether prior data exists. Deduplicate per error type
|
||||
// so multiple failing worktrees don't produce multiple toasts.
|
||||
if (err === "gh_auth" || err === "gh_missing") bridge.notifyError(err)
|
||||
return
|
||||
}
|
||||
const msg = { type: "agentManager.prStatus", worktreeId: id, pr, error: err } as AgentManagerOutMessage
|
||||
bridge["cache"].set(id, msg)
|
||||
bridge["lastErrorNotified"] = undefined
|
||||
host.postToWebview(msg)
|
||||
host.updateWorktreePR(id, pr?.number, pr?.url, pr?.state)
|
||||
},
|
||||
|
||||
@@ -71,9 +71,18 @@ export interface PRComment {
|
||||
createdAt?: number
|
||||
}
|
||||
|
||||
export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented"
|
||||
|
||||
export interface PRReviewer {
|
||||
login: string
|
||||
avatar?: string
|
||||
state: ReviewerState
|
||||
}
|
||||
|
||||
export interface PRStatus {
|
||||
number: number
|
||||
title: string
|
||||
body?: string
|
||||
url: string
|
||||
state: PRState
|
||||
review: ReviewDecision | null
|
||||
@@ -83,12 +92,13 @@ export interface PRStatus {
|
||||
passed: number
|
||||
failed: number
|
||||
pending: number
|
||||
items: PRCheck[]
|
||||
checks: PRCheck[]
|
||||
}
|
||||
reviewers: PRReviewer[]
|
||||
comments?: {
|
||||
total: number
|
||||
unresolved: number
|
||||
items: PRComment[]
|
||||
comments: PRComment[]
|
||||
}
|
||||
additions: number
|
||||
deletions: number
|
||||
@@ -379,6 +389,11 @@ interface PRStatusOutMessage {
|
||||
error?: "gh_missing" | "gh_auth" | "fetch_failed"
|
||||
}
|
||||
|
||||
interface PRErrorOutMessage {
|
||||
type: "agentManager.prError"
|
||||
error: "gh_missing" | "gh_auth" | "fetch_failed"
|
||||
}
|
||||
|
||||
interface ActionOutMessage {
|
||||
type: "action"
|
||||
action: string
|
||||
@@ -418,6 +433,7 @@ export type AgentManagerOutMessage =
|
||||
| RevertWorktreeFileResultMessage
|
||||
| DiffBranchesMessage
|
||||
| PRStatusOutMessage
|
||||
| PRErrorOutMessage
|
||||
| ActionOutMessage
|
||||
| RunStatusMessage
|
||||
| TerminalCreatedMessage
|
||||
@@ -739,6 +755,7 @@ interface OpenPRIn {
|
||||
type: "agentManager.openPR"
|
||||
projectId?: string
|
||||
worktreeId: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
interface OpenSessionsIn {
|
||||
|
||||
@@ -450,7 +450,7 @@ export type WebviewMessage =
|
||||
| { type: "sessionUpdated"; session: ReturnType<typeof sessionToWebview> }
|
||||
| { type: "sessionDeleted"; sessionID: string }
|
||||
| { type: "messageRemoved"; sessionID: string; messageID: string }
|
||||
| { type: "sessionError"; sessionID?: string; error?: unknown }
|
||||
| { type: "sessionError"; eventID: string; sessionID?: string; error?: unknown }
|
||||
| {
|
||||
type: "sandboxStatus"
|
||||
sessionID: string
|
||||
@@ -620,6 +620,7 @@ export function mapSSEEventToWebviewMessage(event: StreamEvent, sessionID: strin
|
||||
case "session.error": {
|
||||
return {
|
||||
type: "sessionError",
|
||||
eventID: event.id,
|
||||
sessionID: event.properties.sessionID,
|
||||
error: event.properties.error,
|
||||
}
|
||||
|
||||
@@ -12,14 +12,7 @@ export const EnvSchema = z
|
||||
.trim()
|
||||
.regex(/^[A-Z_][A-Z0-9_]*$/, INVALID_ENV)
|
||||
|
||||
const VariantConfigSchema = z.object({
|
||||
enable_thinking: z.boolean().optional(),
|
||||
thinking: z.object({ type: z.enum(["enabled", "disabled", "adaptive"]) }).optional(),
|
||||
reasoning_split: z.boolean().optional(),
|
||||
reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(),
|
||||
effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
|
||||
chat_template_args: z.object({ enable_thinking: z.boolean() }).optional(),
|
||||
})
|
||||
const VariantConfigSchema = z.record(z.string(), z.unknown())
|
||||
|
||||
export type VariantConfig = z.infer<typeof VariantConfigSchema>
|
||||
|
||||
|
||||
@@ -80,6 +80,20 @@ test("auto efficient details show server description and model choices", async (
|
||||
await expect(preview).not.toContainText("openai/gpt-5.5")
|
||||
})
|
||||
|
||||
test("auto frontier details show model choices when routes are present", async ({ page }) => {
|
||||
await load(page, "shared--model-selector-accessible")
|
||||
|
||||
await page.getByRole("button", { name: "Review model: Alpha" }).click()
|
||||
await page.getByRole("treeitem", { name: /Kilo Auto Frontier/ }).click()
|
||||
|
||||
const preview = page.locator(".model-selector-preview")
|
||||
await expect(preview).toContainText("Routes each request to the strongest available models.")
|
||||
await expect(preview).toContainText("Model choices")
|
||||
await expect(preview).toContainText("openai/gpt-5.5")
|
||||
await expect(preview).toContainText("anthropic/claude-opus-4.6")
|
||||
await expect(preview).not.toContainText("google/gemini-2.5-flash")
|
||||
})
|
||||
|
||||
test("search uses a flat relevance-ranked result list with provider labels", async ({ page }) => {
|
||||
await load(page, "shared--model-selector-accessible")
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ const TSX_FILES = [
|
||||
path.join(ROOT, "webview-ui/agent-manager/MultiModelSelector.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/pr/PRBadge.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/SectionHeader.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/SidebarSectionHeader.tsx"),
|
||||
path.join(ROOT, "webview-ui/agent-manager/SidebarSearchMenu.tsx"),
|
||||
|
||||
@@ -86,8 +86,8 @@ describe("Agent Manager orchestration domain", () => {
|
||||
url: "https://example.com/pr/42",
|
||||
state: "open",
|
||||
review: "approved",
|
||||
checks: { status: "success", total: 1, passed: 1, failed: 0, pending: 0, items: [] },
|
||||
comments: { total: 2, unresolved: 1, items: [] },
|
||||
checks: { status: "success", total: 1, passed: 1, failed: 0, pending: 0, checks: [] },
|
||||
comments: { total: 2, unresolved: 1, comments: [], reviewers: [] },
|
||||
additions: 10,
|
||||
deletions: 2,
|
||||
files: 1,
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { PRStatusBridge } from "../../src/agent-manager/pr-status-bridge"
|
||||
import type { AgentManagerOutMessage, PRStatus } from "../../src/agent-manager/types"
|
||||
|
||||
const pr: PRStatus = {
|
||||
number: 1,
|
||||
title: "my PR",
|
||||
url: "https://github.com/x/y/pull/1",
|
||||
state: "open",
|
||||
review: null,
|
||||
checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] },
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
files: 0,
|
||||
}
|
||||
|
||||
function harness(opts: { hasPersisted?: boolean } = {}) {
|
||||
const sent: AgentManagerOutMessage[] = []
|
||||
const worktrees: { id: string; prUrl?: string }[] = []
|
||||
const bridge = PRStatusBridge.create({
|
||||
getWorktrees: () => worktrees as never,
|
||||
getWorkspaceRoot: () => "/repo",
|
||||
postToWebview: (msg) => sent.push(msg),
|
||||
updateWorktreePR: () => {},
|
||||
hasPersistedPR: () => opts.hasPersisted ?? false,
|
||||
openExternal: () => {},
|
||||
log: () => {},
|
||||
})
|
||||
const onStatus = (bridge.poller as unknown as { options: { onStatus: (...a: unknown[]) => void } }).options.onStatus
|
||||
return { bridge, sent, onStatus }
|
||||
}
|
||||
|
||||
// --- error deduplication ---
|
||||
|
||||
describe("PRStatusBridge.notifyError", () => {
|
||||
it("sends the first error notification", () => {
|
||||
const { bridge, sent } = harness()
|
||||
bridge.notifyError("gh_missing")
|
||||
expect(sent).toHaveLength(1)
|
||||
expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prError", error: "gh_missing" }))
|
||||
})
|
||||
|
||||
it("deduplicates the same error type", () => {
|
||||
const { bridge, sent } = harness()
|
||||
bridge.notifyError("gh_auth")
|
||||
bridge.notifyError("gh_auth")
|
||||
expect(sent).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("sends again when error type changes", () => {
|
||||
const { bridge, sent } = harness()
|
||||
bridge.notifyError("gh_missing")
|
||||
bridge.notifyError("gh_auth")
|
||||
expect(sent).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
// --- onStatus cache suppression ---
|
||||
|
||||
describe("PRStatusBridge onStatus", () => {
|
||||
it("forwards a successful status to the webview", () => {
|
||||
const { sent, onStatus } = harness()
|
||||
onStatus("wt1", pr)
|
||||
expect(sent).toHaveLength(1)
|
||||
expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prStatus", worktreeId: "wt1", pr }))
|
||||
})
|
||||
|
||||
it("forwards pr:null error when no cache entry and no persisted PR", () => {
|
||||
const { sent, onStatus } = harness()
|
||||
onStatus("wt1", null, "fetch_failed")
|
||||
expect(sent).toHaveLength(1)
|
||||
expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prStatus", worktreeId: "wt1", pr: null }))
|
||||
})
|
||||
|
||||
it("suppresses pr:null error when cache entry exists", () => {
|
||||
const { sent, onStatus } = harness()
|
||||
onStatus("wt1", pr)
|
||||
sent.length = 0
|
||||
onStatus("wt1", null, "fetch_failed")
|
||||
expect(sent).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("suppresses pr:null error when persisted PR exists", () => {
|
||||
const { sent, onStatus } = harness({ hasPersisted: true })
|
||||
onStatus("wt1", null, "fetch_failed")
|
||||
expect(sent).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("forwards gh_auth error even when cache entry exists", () => {
|
||||
const { sent, onStatus } = harness()
|
||||
onStatus("wt1", pr)
|
||||
sent.length = 0
|
||||
onStatus("wt1", null, "gh_auth")
|
||||
const errorMsg = sent.find((m) => m.type === "agentManager.prError")
|
||||
expect(errorMsg).toEqual(expect.objectContaining({ error: "gh_auth" }))
|
||||
})
|
||||
|
||||
it("forwards gh_missing error even when cache entry exists", () => {
|
||||
const { sent, onStatus } = harness()
|
||||
onStatus("wt1", pr)
|
||||
sent.length = 0
|
||||
onStatus("wt1", null, "gh_missing")
|
||||
const errorMsg = sent.find((m) => m.type === "agentManager.prError")
|
||||
expect(errorMsg).toEqual(expect.objectContaining({ error: "gh_missing" }))
|
||||
})
|
||||
})
|
||||
|
||||
// --- replay ---
|
||||
|
||||
describe("PRStatusBridge.replay", () => {
|
||||
it("replays cached status messages", () => {
|
||||
const { bridge, sent, onStatus } = harness()
|
||||
onStatus("wt1", pr)
|
||||
sent.length = 0
|
||||
bridge.replay()
|
||||
expect(sent).toHaveLength(1)
|
||||
expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prStatus", worktreeId: "wt1" }))
|
||||
})
|
||||
|
||||
it("replays the last auth error on reconnect", () => {
|
||||
const { bridge, sent, onStatus } = harness()
|
||||
onStatus("wt1", null, "gh_auth")
|
||||
sent.length = 0
|
||||
bridge.replay()
|
||||
expect(
|
||||
sent.some((m) => m.type === "agentManager.prError" && (m as never as { error: string }).error === "gh_auth"),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("does not replay fetch_failed errors", () => {
|
||||
const { bridge, sent, onStatus } = harness()
|
||||
onStatus("wt1", null, "fetch_failed")
|
||||
sent.length = 0
|
||||
bridge.replay()
|
||||
expect(sent).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
// --- snapshot ---
|
||||
|
||||
describe("PRStatusBridge.snapshot", () => {
|
||||
it("returns only entries with a non-null pr", () => {
|
||||
const { bridge, onStatus } = harness()
|
||||
onStatus("wt1", pr)
|
||||
onStatus("wt2", pr)
|
||||
expect(bridge.snapshot().size).toBe(2)
|
||||
})
|
||||
|
||||
it("excludes entries where pr was null", () => {
|
||||
const { bridge, onStatus } = harness({ hasPersisted: true })
|
||||
onStatus("wt1", null, "fetch_failed")
|
||||
expect(bridge.snapshot().size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
// --- remove / reset ---
|
||||
|
||||
describe("PRStatusBridge.remove", () => {
|
||||
it("removes a cached entry so it is no longer replayed", () => {
|
||||
const { bridge, sent, onStatus } = harness()
|
||||
onStatus("wt1", pr)
|
||||
bridge.remove("wt1")
|
||||
sent.length = 0
|
||||
bridge.replay()
|
||||
expect(sent).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("PRStatusBridge.reset", () => {
|
||||
it("clears cache and error state so replay sends nothing", () => {
|
||||
const { bridge, sent, onStatus } = harness()
|
||||
onStatus("wt1", pr)
|
||||
bridge.notifyError("gh_auth")
|
||||
bridge.reset()
|
||||
sent.length = 0
|
||||
bridge.replay()
|
||||
expect(sent).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("allows the same error to be sent again after reset", () => {
|
||||
const { bridge, sent } = harness()
|
||||
bridge.notifyError("gh_auth")
|
||||
bridge.reset()
|
||||
sent.length = 0
|
||||
bridge.notifyError("gh_auth")
|
||||
expect(sent).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,324 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import {
|
||||
parsePRResult,
|
||||
checkStatus,
|
||||
formatCheckDuration,
|
||||
parseComments,
|
||||
parseReviewers,
|
||||
} from "../../src/agent-manager/am-pr-utils"
|
||||
import type { GhThread, GhReviewRequest, GhReview } from "../../src/agent-manager/am-pr-types"
|
||||
|
||||
// --- parsePRResult ---
|
||||
|
||||
describe("parsePRResult", () => {
|
||||
it("returns null when number is missing", () => {
|
||||
expect(parsePRResult(JSON.stringify({ title: "foo" }))).toBeNull()
|
||||
})
|
||||
|
||||
it("parses an open PR", () => {
|
||||
const raw = {
|
||||
number: 42,
|
||||
title: "my PR",
|
||||
body: "desc",
|
||||
url: "https://github.com/x/y/pull/42",
|
||||
state: "OPEN",
|
||||
isDraft: false,
|
||||
reviewDecision: null,
|
||||
additions: 10,
|
||||
deletions: 3,
|
||||
changedFiles: 2,
|
||||
}
|
||||
expect(parsePRResult(JSON.stringify(raw))).toEqual({
|
||||
number: 42,
|
||||
title: "my PR",
|
||||
body: "desc",
|
||||
url: "https://github.com/x/y/pull/42",
|
||||
state: "open",
|
||||
review: null,
|
||||
additions: 10,
|
||||
deletions: 3,
|
||||
files: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it("maps isDraft to draft state regardless of gh state field", () => {
|
||||
const raw = {
|
||||
number: 1,
|
||||
title: "",
|
||||
body: "",
|
||||
url: "",
|
||||
state: "OPEN",
|
||||
isDraft: true,
|
||||
reviewDecision: null,
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
changedFiles: 0,
|
||||
}
|
||||
expect(parsePRResult(JSON.stringify(raw))?.state).toBe("draft")
|
||||
})
|
||||
|
||||
it("maps MERGED state", () => {
|
||||
const raw = {
|
||||
number: 1,
|
||||
title: "",
|
||||
body: "",
|
||||
url: "",
|
||||
state: "MERGED",
|
||||
isDraft: false,
|
||||
reviewDecision: null,
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
changedFiles: 0,
|
||||
}
|
||||
expect(parsePRResult(JSON.stringify(raw))?.state).toBe("merged")
|
||||
})
|
||||
|
||||
it("maps CLOSED state", () => {
|
||||
const raw = {
|
||||
number: 1,
|
||||
title: "",
|
||||
body: "",
|
||||
url: "",
|
||||
state: "CLOSED",
|
||||
isDraft: false,
|
||||
reviewDecision: null,
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
changedFiles: 0,
|
||||
}
|
||||
expect(parsePRResult(JSON.stringify(raw))?.state).toBe("closed")
|
||||
})
|
||||
|
||||
it("maps APPROVED review decision", () => {
|
||||
const raw = {
|
||||
number: 1,
|
||||
title: "",
|
||||
body: "",
|
||||
url: "",
|
||||
state: "OPEN",
|
||||
isDraft: false,
|
||||
reviewDecision: "APPROVED",
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
changedFiles: 0,
|
||||
}
|
||||
expect(parsePRResult(JSON.stringify(raw))?.review).toBe("approved")
|
||||
})
|
||||
|
||||
it("maps CHANGES_REQUESTED review decision", () => {
|
||||
const raw = {
|
||||
number: 1,
|
||||
title: "",
|
||||
body: "",
|
||||
url: "",
|
||||
state: "OPEN",
|
||||
isDraft: false,
|
||||
reviewDecision: "CHANGES_REQUESTED",
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
changedFiles: 0,
|
||||
}
|
||||
expect(parsePRResult(JSON.stringify(raw))?.review).toBe("changes_requested")
|
||||
})
|
||||
|
||||
it("maps REVIEW_REQUIRED review decision to pending", () => {
|
||||
const raw = {
|
||||
number: 1,
|
||||
title: "",
|
||||
body: "",
|
||||
url: "",
|
||||
state: "OPEN",
|
||||
isDraft: false,
|
||||
reviewDecision: "REVIEW_REQUIRED",
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
changedFiles: 0,
|
||||
}
|
||||
expect(parsePRResult(JSON.stringify(raw))?.review).toBe("pending")
|
||||
})
|
||||
|
||||
it("returns null review for unknown decision", () => {
|
||||
const raw = {
|
||||
number: 1,
|
||||
title: "",
|
||||
body: "",
|
||||
url: "",
|
||||
state: "OPEN",
|
||||
isDraft: false,
|
||||
reviewDecision: "SOMETHING_ELSE",
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
changedFiles: 0,
|
||||
}
|
||||
expect(parsePRResult(JSON.stringify(raw))?.review).toBeNull()
|
||||
})
|
||||
|
||||
it("defaults missing fields to empty strings and zeros", () => {
|
||||
const result = parsePRResult(JSON.stringify({ number: 5 }))
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({ title: "", body: "", url: "", additions: 0, deletions: 0, files: 0 }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// --- checkStatus ---
|
||||
|
||||
describe("checkStatus", () => {
|
||||
it("maps SUCCESS", () => expect(checkStatus("SUCCESS")).toBe("success"))
|
||||
it("maps FAILURE", () => expect(checkStatus("FAILURE")).toBe("failure"))
|
||||
it("maps ERROR to failure", () => expect(checkStatus("ERROR")).toBe("failure"))
|
||||
it("maps PENDING", () => expect(checkStatus("PENDING")).toBe("pending"))
|
||||
it("maps QUEUED to pending", () => expect(checkStatus("QUEUED")).toBe("pending"))
|
||||
it("maps IN_PROGRESS to pending", () => expect(checkStatus("IN_PROGRESS")).toBe("pending"))
|
||||
it("maps REQUESTED to pending", () => expect(checkStatus("REQUESTED")).toBe("pending"))
|
||||
it("maps WAITING to pending", () => expect(checkStatus("WAITING")).toBe("pending"))
|
||||
it("maps SKIPPED", () => expect(checkStatus("SKIPPED")).toBe("skipped"))
|
||||
it("maps CANCELLED", () => expect(checkStatus("CANCELLED")).toBe("cancelled"))
|
||||
it("maps TIMED_OUT to cancelled", () => expect(checkStatus("TIMED_OUT")).toBe("cancelled"))
|
||||
it("maps STALE to cancelled", () => expect(checkStatus("STALE")).toBe("cancelled"))
|
||||
it("maps STARTUP_FAILURE to cancelled", () => expect(checkStatus("STARTUP_FAILURE")).toBe("cancelled"))
|
||||
it("maps unknown state to pending", () => expect(checkStatus("WHATEVER")).toBe("pending"))
|
||||
it("is case-insensitive", () => expect(checkStatus("success")).toBe("success"))
|
||||
})
|
||||
|
||||
// --- formatCheckDuration ---
|
||||
|
||||
describe("formatCheckDuration", () => {
|
||||
it("returns undefined when startedAt is missing", () => {
|
||||
expect(formatCheckDuration(undefined, "2024-01-01T00:01:00Z")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("returns undefined when completedAt is missing", () => {
|
||||
expect(formatCheckDuration("2024-01-01T00:00:00Z", undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("formats sub-minute durations in seconds", () => {
|
||||
expect(formatCheckDuration("2024-01-01T00:00:00Z", "2024-01-01T00:00:45Z")).toBe("45s")
|
||||
})
|
||||
|
||||
it("formats durations over a minute as m/s", () => {
|
||||
expect(formatCheckDuration("2024-01-01T00:00:00Z", "2024-01-01T00:02:30Z")).toBe("2m 30s")
|
||||
})
|
||||
|
||||
it("formats exactly 60 seconds as 1m 0s", () => {
|
||||
expect(formatCheckDuration("2024-01-01T00:00:00Z", "2024-01-01T00:01:00Z")).toBe("1m 0s")
|
||||
})
|
||||
})
|
||||
|
||||
// --- parseComments ---
|
||||
|
||||
describe("parseComments", () => {
|
||||
it("returns empty array for empty threads", () => {
|
||||
expect(parseComments([])).toEqual([])
|
||||
})
|
||||
|
||||
it("skips threads with no comments", () => {
|
||||
const threads: GhThread[] = [{ isResolved: false, comments: { nodes: [] } }]
|
||||
expect(parseComments(threads)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("parses a resolved thread", () => {
|
||||
const threads: GhThread[] = [
|
||||
{
|
||||
isResolved: true,
|
||||
comments: {
|
||||
nodes: [
|
||||
{
|
||||
id: "c1",
|
||||
author: { login: "alice", avatarUrl: "https://avatar" },
|
||||
body: "looks good",
|
||||
path: "src/foo.ts",
|
||||
line: 10,
|
||||
url: "https://url",
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
expect(parseComments(threads)).toEqual([
|
||||
{
|
||||
id: "c1",
|
||||
author: "alice",
|
||||
avatar: "https://avatar",
|
||||
body: "looks good",
|
||||
file: "src/foo.ts",
|
||||
line: 10,
|
||||
url: "https://url",
|
||||
resolved: true,
|
||||
createdAt: new Date("2024-01-01T00:00:00Z").getTime(),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("defaults missing author to 'unknown'", () => {
|
||||
const threads: GhThread[] = [{ isResolved: false, comments: { nodes: [{ id: "c2", body: "note" }] } }]
|
||||
expect(parseComments(threads)[0]?.author).toBe("unknown")
|
||||
})
|
||||
|
||||
it("only uses the first comment of each thread", () => {
|
||||
const threads: GhThread[] = [
|
||||
{
|
||||
isResolved: false,
|
||||
comments: {
|
||||
nodes: [
|
||||
{ id: "first", body: "first comment" },
|
||||
{ id: "second", body: "second comment" },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
const result = parseComments(threads)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]?.id).toBe("first")
|
||||
})
|
||||
})
|
||||
|
||||
// --- parseReviewers ---
|
||||
|
||||
describe("parseReviewers", () => {
|
||||
it("returns empty array with no requests or reviews", () => {
|
||||
expect(parseReviewers([], [])).toEqual([])
|
||||
})
|
||||
|
||||
it("adds pending reviewer from request", () => {
|
||||
const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice", avatarUrl: "https://avatar" } }]
|
||||
expect(parseReviewers(requests, [])).toEqual([{ login: "alice", avatar: "https://avatar", state: "pending" }])
|
||||
})
|
||||
|
||||
it("skips review requests without a login", () => {
|
||||
const requests: GhReviewRequest[] = [{ requestedReviewer: {} }]
|
||||
expect(parseReviewers(requests, [])).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("adds reviewer from review when not in requests", () => {
|
||||
const reviews: GhReview[] = [{ author: { login: "bob" }, state: "APPROVED" }]
|
||||
expect(parseReviewers([], reviews)).toEqual([{ login: "bob", avatar: undefined, state: "approved" }])
|
||||
})
|
||||
|
||||
it("upgrades pending request to approved when review arrives", () => {
|
||||
const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice" } }]
|
||||
const reviews: GhReview[] = [{ author: { login: "alice" }, state: "APPROVED" }]
|
||||
expect(parseReviewers(requests, reviews)).toEqual([{ login: "alice", avatar: undefined, state: "approved" }])
|
||||
})
|
||||
|
||||
it("does not downgrade approved to commented", () => {
|
||||
const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice" } }]
|
||||
const reviews: GhReview[] = [
|
||||
{ author: { login: "alice" }, state: "APPROVED" },
|
||||
{ author: { login: "alice" }, state: "COMMENTED" },
|
||||
]
|
||||
expect(parseReviewers(requests, reviews)[0]?.state).toBe("approved")
|
||||
})
|
||||
|
||||
it("does upgrade pending to changes_requested", () => {
|
||||
const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice" } }]
|
||||
const reviews: GhReview[] = [{ author: { login: "alice" }, state: "CHANGES_REQUESTED" }]
|
||||
expect(parseReviewers(requests, reviews)[0]?.state).toBe("changes_requested")
|
||||
})
|
||||
|
||||
it("skips reviews without a login", () => {
|
||||
const reviews: GhReview[] = [{ author: {}, state: "APPROVED" }]
|
||||
expect(parseReviewers([], reviews)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -93,7 +93,7 @@ describe("validateCustomProvider – variant name validation", () => {
|
||||
]
|
||||
const out = validateCustomProvider(args(form))
|
||||
expect(out.result).toBeUndefined()
|
||||
expect(out.errors.models[0].variants?.[0]?.name).toBe("provider.custom.error.required")
|
||||
expect(out.errors.models[0].variants?.[0]?.name).toBe('variants[""]: provider.custom.error.required')
|
||||
})
|
||||
|
||||
it("blocks submit and reports error when reasoning is enabled with a whitespace-only variant name", () => {
|
||||
@@ -112,7 +112,7 @@ describe("validateCustomProvider – variant name validation", () => {
|
||||
]
|
||||
const out = validateCustomProvider(args(form))
|
||||
expect(out.result).toBeUndefined()
|
||||
expect(out.errors.models[0].variants?.[0]?.name).toBe("provider.custom.error.required")
|
||||
expect(out.errors.models[0].variants?.[0]?.name).toBe('variants[" "]: provider.custom.error.required')
|
||||
})
|
||||
|
||||
it("blocks submit and reports duplicate error for two variants with the same name", () => {
|
||||
@@ -140,7 +140,7 @@ describe("validateCustomProvider – variant name validation", () => {
|
||||
]
|
||||
const out = validateCustomProvider(args(form))
|
||||
expect(out.result).toBeUndefined()
|
||||
expect(out.errors.models[0].variants?.[1]?.name).toBe("provider.custom.error.duplicate")
|
||||
expect(out.errors.models[0].variants?.[1]?.name).toBe('variants["fast"]: provider.custom.error.duplicate')
|
||||
})
|
||||
|
||||
it("ignores variants entirely when reasoning is disabled, even if they have empty names", () => {
|
||||
@@ -206,6 +206,34 @@ describe("validateCustomProvider – variant name validation", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves opaque variant options after the editor controls are removed", () => {
|
||||
const form = base()
|
||||
const raw = {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
customOption: { enabled: true },
|
||||
}
|
||||
form.models[0].reasoning = true
|
||||
form.models[0].variants = [
|
||||
{
|
||||
name: "high",
|
||||
raw,
|
||||
enableThinking: undefined,
|
||||
thinking: "adaptive",
|
||||
splitReasoning: undefined,
|
||||
outputEffort: undefined,
|
||||
reasoningEffort: undefined,
|
||||
chatTemplateArgs: undefined,
|
||||
},
|
||||
]
|
||||
|
||||
const out = validateCustomProvider(args(form))
|
||||
expect(out.result).toBeDefined()
|
||||
const saved = out.result!.config.models["model-1"] as Record<string, unknown>
|
||||
expect(saved.variants).toEqual({ high: raw })
|
||||
})
|
||||
|
||||
it("serializes image modality when supportsImages is set", () => {
|
||||
const form = base()
|
||||
form.models[0].supportsImages = true
|
||||
@@ -264,4 +292,20 @@ describe("validateCustomProvider – variant name validation", () => {
|
||||
const saved = out.result!.config.models["model-1"] as Record<string, unknown>
|
||||
expect(saved.modalities).toEqual({ input: ["text", "audio", "video", "pdf"], output: ["text", "audio"] })
|
||||
})
|
||||
|
||||
it("handles multiple models with reasoning and images toggled", () => {
|
||||
const form = base()
|
||||
form.models = [
|
||||
{ id: "m1", name: "Model 1", reasoning: true, supportsImages: true, modalities: {}, variants: [] },
|
||||
{ id: "m2", name: "Model 2", reasoning: true, supportsImages: false, modalities: {}, variants: [] },
|
||||
]
|
||||
const out = validateCustomProvider(args(form))
|
||||
expect(out.result).toBeDefined()
|
||||
const m1 = out.result!.config.models["m1"] as Record<string, unknown>
|
||||
const m2 = out.result!.config.models["m2"] as Record<string, unknown>
|
||||
expect(m1.reasoning).toBe(true)
|
||||
expect(m1.modalities).toEqual({ input: ["text", "image"] })
|
||||
expect(m2.reasoning).toBe(true)
|
||||
expect(m2.modalities).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -162,6 +162,35 @@ describe("sanitizeCustomProviderConfig", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves opaque options on existing variants", () => {
|
||||
const variant = {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
reasoningEffort: "max",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
customOption: { enabled: true },
|
||||
}
|
||||
const result = sanitizeCustomProviderConfig({
|
||||
name: "Thinking Provider",
|
||||
options: { baseURL: "https://example.com/v1" },
|
||||
models: {
|
||||
"model-1": {
|
||||
name: "Model One",
|
||||
variants: { high: variant },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
value: {
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
name: "Thinking Provider",
|
||||
options: { baseURL: "https://example.com/v1" },
|
||||
models: { "model-1": { name: "Model One", variants: { high: variant } } },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves core custom model modalities", () => {
|
||||
const result = sanitizeCustomProviderConfig({
|
||||
name: "Media Provider",
|
||||
|
||||
@@ -127,6 +127,43 @@ describe("KiloProvider memory events", () => {
|
||||
expect(calls).toEqual(["/repo", "/repo"])
|
||||
})
|
||||
|
||||
it("refreshes status without forwarding transient memory errors", async () => {
|
||||
const calls: string[] = []
|
||||
const posts: unknown[] = []
|
||||
const client = {
|
||||
memory: {
|
||||
status: async (input: { directory: string }) => {
|
||||
calls.push(input.directory)
|
||||
return { data: status(input.directory) }
|
||||
},
|
||||
},
|
||||
} as unknown as KiloClient
|
||||
const provider = new KiloProvider(
|
||||
{} as never,
|
||||
{
|
||||
getClient: () => client,
|
||||
} as never,
|
||||
)
|
||||
const item = provider as unknown as Internals
|
||||
item.webview = { postMessage: async (message) => posts.push(message) }
|
||||
item.currentSession = { id: "ses_active" }
|
||||
item.trackedSessionIds.add("ses_active")
|
||||
provider.setSessionDirectory("ses_active", "/repo")
|
||||
|
||||
item.handleEvent(
|
||||
{
|
||||
type: "memory.error",
|
||||
properties: { sessionID: "ses_active", reason: "transient" },
|
||||
},
|
||||
"/repo",
|
||||
)
|
||||
await item.memory.idle()
|
||||
|
||||
expect(posts).not.toContainEqual(expect.objectContaining({ type: "memoryEvent" }))
|
||||
expect(posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", sessionID: "ses_active" }))
|
||||
expect(calls).toEqual(["/repo"])
|
||||
})
|
||||
|
||||
it("uses the project directory when toggling memory", async () => {
|
||||
const calls: unknown[] = []
|
||||
const client = {
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
Event,
|
||||
EventSessionStatus,
|
||||
EventSessionTurnClose,
|
||||
EventSessionError,
|
||||
EventSandboxStatusChanged,
|
||||
EventPermissionAsked,
|
||||
EventPermissionReplied,
|
||||
@@ -375,6 +376,30 @@ describe("mapSSEEventToWebviewMessage", () => {
|
||||
expect(msg).toEqual({ type: "sessionTurnClosed", sessionID: "sess-1", reason: "interrupted" })
|
||||
})
|
||||
|
||||
it("maps session errors with their event identity and message", () => {
|
||||
const event: EventSessionError = {
|
||||
id: "evt-error",
|
||||
type: "session.error",
|
||||
properties: {
|
||||
sessionID: "sess-1",
|
||||
error: {
|
||||
name: "APIError",
|
||||
data: {
|
||||
message: "prompt_cache_breakpoint is not supported on this model",
|
||||
isRetryable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(mapSSEEventToWebviewMessage(event, "sess-1")).toEqual({
|
||||
type: "sessionError",
|
||||
eventID: "evt-error",
|
||||
sessionID: "sess-1",
|
||||
error: event.properties.error,
|
||||
})
|
||||
})
|
||||
|
||||
it("maps permission.asked to permissionRequest", () => {
|
||||
const event: EventPermissionAsked = {
|
||||
type: "permission.asked",
|
||||
|
||||
@@ -18,6 +18,11 @@ describe("fmtPrice", () => {
|
||||
expect(fmtPrice(0)).toBe("Free")
|
||||
})
|
||||
|
||||
it("returns dash for negative price", () => {
|
||||
expect(fmtPrice(-1)).toBe("—")
|
||||
expect(fmtPrice(-1000000)).toBe("—")
|
||||
})
|
||||
|
||||
it("uses 4 decimal places for sub-cent prices", () => {
|
||||
expect(fmtPrice(0.005)).toBe("$0.0050/1M")
|
||||
})
|
||||
|
||||
@@ -128,7 +128,7 @@ describe("isAuto", () => {
|
||||
})
|
||||
|
||||
describe("autoChoices", () => {
|
||||
it("uses backend Auto Efficient routes and resolves names when available", () => {
|
||||
it("uses backend Auto routes and resolves names when available", () => {
|
||||
expect(
|
||||
autoChoices(
|
||||
{
|
||||
@@ -144,12 +144,32 @@ describe("autoChoices", () => {
|
||||
])
|
||||
})
|
||||
|
||||
it("ignores missing routes and non-efficient Auto models", () => {
|
||||
it("shows routes for any Auto model when present", () => {
|
||||
expect(
|
||||
autoChoices(
|
||||
{
|
||||
providerID: KILO_GATEWAY_ID,
|
||||
id: "kilo-auto/frontier",
|
||||
autoRouting: { models: ["provider/model"] },
|
||||
},
|
||||
[{ id: "provider/model", name: "Provider: Model" }],
|
||||
),
|
||||
).toEqual([{ id: "provider/model", name: "Model" }])
|
||||
expect(
|
||||
autoChoices({
|
||||
providerID: KILO_GATEWAY_ID,
|
||||
id: "kilo-auto/free",
|
||||
autoRouting: { models: ["provider/model"] },
|
||||
}),
|
||||
).toEqual([{ id: "provider/model", name: "provider/model" }])
|
||||
})
|
||||
|
||||
it("ignores missing routes and non-Auto models", () => {
|
||||
expect(autoChoices({ providerID: KILO_GATEWAY_ID, id: "kilo-auto/efficient" })).toEqual([])
|
||||
expect(
|
||||
autoChoices({
|
||||
providerID: KILO_GATEWAY_ID,
|
||||
id: "kilo-auto/frontier",
|
||||
id: "anthropic/claude-sonnet",
|
||||
autoRouting: { models: ["provider/model"] },
|
||||
}),
|
||||
).toEqual([])
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { configMessage } from "../../webview-ui/src/utils/open-config"
|
||||
|
||||
describe("configMessage", () => {
|
||||
it("builds a global config request with localized labels", () => {
|
||||
const message = configMessage("global", (key, params) => `${key}:${params?.scope ?? ""}`)
|
||||
|
||||
expect(message.type).toBe("openConfigFile")
|
||||
expect(message.scope).toBe("global")
|
||||
expect(message.labels.scope).toBe("settings.config.scope.global:")
|
||||
expect(message.labels.title).toBe("settings.config.title:settings.config.scope.global:")
|
||||
expect(message.labels.openFailed).toBe("settings.config.openFailed:settings.config.scope.global:")
|
||||
})
|
||||
})
|
||||
@@ -206,6 +206,27 @@ describe("saveCustomProvider", () => {
|
||||
expect(calls.set).toEqual([{ providerID: "myprovider", auth: { type: "api", key: "sk-test" } }])
|
||||
})
|
||||
|
||||
it("preserves opaque existing variant options through the save boundary", async () => {
|
||||
const variant = {
|
||||
thinking: { type: "adaptive", display: "summarized" },
|
||||
reasoningEffort: "custom",
|
||||
reasoningSummary: "auto",
|
||||
include: ["reasoning.encrypted_content"],
|
||||
customOption: { enabled: true },
|
||||
}
|
||||
const saved = {
|
||||
...createSavedProvider(),
|
||||
models: { "model-1": { name: "Model One", reasoning: true, variants: { high: variant } } },
|
||||
}
|
||||
const existing = { disabled_providers: [], provider: { myprovider: saved } }
|
||||
const { ctx, calls, setCachedConfig } = createCtx(existing)
|
||||
|
||||
await saveCustomProvider(ctx, "req", "myprovider", saved, undefined, false, null, setCachedConfig)
|
||||
|
||||
const provider = (calls.config[0]?.config.provider as Record<string, typeof saved>).myprovider
|
||||
expect(provider.models["model-1"].variants.high).toEqual(variant)
|
||||
})
|
||||
|
||||
// Regression tests for https://github.com/Kilo-Org/kilocode/issues/9186
|
||||
//
|
||||
// The CLI's config.update endpoint deep-merges its payload with the existing
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { errorIDs, visibleError } from "../../webview-ui/src/context/session-errors"
|
||||
import {
|
||||
errorIDs,
|
||||
preserveSessionErrors,
|
||||
visibleError,
|
||||
withoutResolvedSessionErrors,
|
||||
} from "../../webview-ui/src/context/session-errors"
|
||||
import type { Message } from "../../webview-ui/src/types/messages"
|
||||
|
||||
const base = {
|
||||
@@ -45,3 +50,32 @@ describe("visibleError", () => {
|
||||
expect(visibleError(messages, () => false)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("session error reconciliation", () => {
|
||||
const error = {
|
||||
name: "APIError",
|
||||
data: { message: "prompt_cache_breakpoint is not supported on this model", isRetryable: false },
|
||||
}
|
||||
|
||||
it("preserves transient errors across stale history replacements", () => {
|
||||
const user = { ...base, id: "message_1", role: "user" as const }
|
||||
const transient = { ...assistant("message_2", error), parentID: user.id, sessionErrorID: "event_1" }
|
||||
|
||||
expect(preserveSessionErrors([user, transient], [user])).toEqual([user, transient])
|
||||
})
|
||||
|
||||
it("replaces a transient error with its persisted assistant message", () => {
|
||||
const transient = { ...assistant("message_2", error), parentID: "message_1", sessionErrorID: "event_1" }
|
||||
const persisted = { ...assistant("message_3", error), parentID: "message_1" }
|
||||
|
||||
expect(preserveSessionErrors([transient], [persisted])).toEqual([persisted])
|
||||
expect(withoutResolvedSessionErrors([transient], [persisted])).toEqual([])
|
||||
})
|
||||
|
||||
it("deduplicates repeated delivery of the same session error event", () => {
|
||||
const first = { ...assistant("message_2", error), parentID: "message_1", sessionErrorID: "event_1" }
|
||||
const repeat = { ...assistant("message_3", error), parentID: "message_1", sessionErrorID: "event_1" }
|
||||
|
||||
expect(withoutResolvedSessionErrors([first], [repeat])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
RunStatus,
|
||||
PRStatus,
|
||||
AgentManagerPRStatusMessage,
|
||||
AgentManagerPRErrorMessage,
|
||||
AgentManagerProjectsMessage,
|
||||
AgentProjectSnapshot,
|
||||
ManagedSessionState,
|
||||
@@ -143,6 +144,7 @@ import {
|
||||
import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering"
|
||||
import { useTabScroll } from "./tab-scroll"
|
||||
import { DiffPanel } from "./DiffPanel"
|
||||
import { PRPanel } from "./pr/PRPanel"
|
||||
import { createRevertFile } from "./revert-file"
|
||||
import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView"
|
||||
import { createApplyToLocal } from "./apply-to-local"
|
||||
@@ -152,6 +154,8 @@ import { clearReviewComposer, createReviewComposer } from "../diff-viewer/review
|
||||
import type { SidebarSearchMenuRef } from "./SidebarSearchMenu"
|
||||
import { createSidebarSearch, type SidebarSearchItem } from "./sidebar-search"
|
||||
import { randomColor } from "./section-colors"
|
||||
import { createMarkdownRender } from "./review-preferences"
|
||||
import { createSidebarCollapse } from "./sidebar-collapse"
|
||||
import { createNewTaskDrafts } from "./new-task-drafts"
|
||||
import {
|
||||
buildTopLevelItems,
|
||||
@@ -170,11 +174,9 @@ import { DiffScopeControls } from "../diff-viewer/DiffScopeControls"
|
||||
import { scopeCapabilities } from "./diff-scope-state"
|
||||
import { createDiffReviewScope } from "./diff-review-scope"
|
||||
import { initialMessage, seedInitialVariant } from "./initial-message"
|
||||
import { createMarkdownRender } from "./review-preferences"
|
||||
import { createSidebarCollapse } from "./sidebar-collapse"
|
||||
import { SidebarToggleButton } from "./SidebarToggleButton"
|
||||
import { setTabWidths } from "./tab-widths"
|
||||
import { clampPanelWidth, maxPanelWidth, minPanelWidth } from "./side-panel-layout"
|
||||
import { clampPanelWidth, maxPanelWidth, minPanelWidth, SidePanel } from "./side-panel-layout"
|
||||
import { buildShortcutCategories } from "./shortcuts"
|
||||
import { tracker } from "./telemetry"
|
||||
import { createChatFocus, hasQuestionOption } from "./focus"
|
||||
@@ -195,7 +197,7 @@ interface SetupState {
|
||||
|
||||
/** Sidebar selection: LOCAL for local repo, worktree ID for a worktree, or null for an unassigned session. */
|
||||
type SidebarSelection = typeof LOCAL | string | null
|
||||
type SidePanel = "diff" | "pr" | "terminal" | null
|
||||
export type SidePanelState = SidePanel | null
|
||||
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
|
||||
const SIDE_RESIZE_INTERVAL_MS = 32
|
||||
|
||||
@@ -297,8 +299,16 @@ const AgentManagerContent: Component = () => {
|
||||
let sideResizeTime = 0
|
||||
|
||||
const [history, setHistory] = createSignal(false)
|
||||
const [sidePanel, setSidePanel] = createSignal<SidePanel>(null)
|
||||
const diffOpen = () => sidePanel() === "diff"
|
||||
const [sidePanel, setSidePanel] = createSignal<SidePanelState>(null)
|
||||
const diffOpen = () => sidePanel() === SidePanel.Diff
|
||||
const prOpen = () => sidePanel() === SidePanel.PR
|
||||
const activePR = createMemo(() => {
|
||||
const selected = selection()
|
||||
if (!selected || selected === LOCAL) return undefined
|
||||
const pr = prStatuses()[selected]
|
||||
if (!pr) return undefined
|
||||
return { pr, selected, wt: worktrees().find((w) => w.id === selected) }
|
||||
})
|
||||
const diffs = createWorktreeDiffs(vscode)
|
||||
const diffDatas = diffs.diffDatas
|
||||
const diffLoading = diffs.diffLoading
|
||||
@@ -324,7 +334,7 @@ const AgentManagerContent: Component = () => {
|
||||
const showSideTerminal = () => {
|
||||
setHistory(false)
|
||||
setReviewActive(false)
|
||||
setSidePanel("terminal")
|
||||
setSidePanel(SidePanel.Terminal)
|
||||
}
|
||||
|
||||
const [reviewOpenByContext, setReviewOpenByContext] = createSignal<Record<string, boolean>>({})
|
||||
@@ -336,7 +346,6 @@ const AgentManagerContent: Component = () => {
|
||||
// Per-worktree git stats (diff additions/deletions, commits missing from origin)
|
||||
const worktreeStats = () => registry.active().worktreeStats()
|
||||
|
||||
// Per-worktree PR status data
|
||||
const prStatuses = () => registry.active().prStatuses()
|
||||
|
||||
const runStatuses = () => registry.active().runStatuses()
|
||||
@@ -421,7 +430,7 @@ const AgentManagerContent: Component = () => {
|
||||
const rememberPromptFocus = (focused: boolean) => {
|
||||
if (focused) focusMemory.set(focusKey(), "prompt")
|
||||
}
|
||||
const terminalVisible = () => sidePanel() === "terminal" && !history() && !reviewActive()
|
||||
const terminalVisible = () => sidePanel() === SidePanel.Terminal && !history() && !reviewActive()
|
||||
const focusOnDraftChange = () => {
|
||||
const key = focusKey()
|
||||
const owner = focusMemory.get(key)
|
||||
@@ -490,6 +499,15 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
createEffect(on(selection, () => cancelPendingDelete(), { defer: true }))
|
||||
createEffect(on(selection, () => clearReviewComposer(reviewComposer), { defer: true }))
|
||||
createEffect(
|
||||
on(
|
||||
selection,
|
||||
() => {
|
||||
if (!activePR()) setSidePanel((p) => (p === SidePanel.PR ? null : p))
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
onCleanup(() => clearTimeout(pendingDeleteTimer))
|
||||
|
||||
// Per-context tab memory lives in the active project's store: maps sidebar
|
||||
@@ -547,11 +565,17 @@ const AgentManagerContent: Component = () => {
|
||||
}
|
||||
const openWindow = metrics.click("open_worktree_window", "tab_toolbar", openWorktreeDirectory)
|
||||
|
||||
const togglePRPanel = () => {
|
||||
setHistory(false)
|
||||
if (reviewActive()) closeReviewTab()
|
||||
setSidePanel((prev) => (prev === SidePanel.PR ? null : SidePanel.PR))
|
||||
}
|
||||
|
||||
const openSelectedPR = () => {
|
||||
const sel = selection()
|
||||
if (!sel || sel === LOCAL || !prStatuses()[sel]) return
|
||||
metrics.track("open_pull_request", "keyboard_shortcut")
|
||||
vscode.postMessage({ type: "agentManager.openPR", worktreeId: sel })
|
||||
togglePRPanel()
|
||||
}
|
||||
|
||||
const runWorktree = (id: string, destination: TerminalDestination) => {
|
||||
@@ -1183,8 +1207,8 @@ const AgentManagerContent: Component = () => {
|
||||
} else if (msg.action === "toggleDiff") {
|
||||
if (reviewActive()) {
|
||||
closeReviewTab()
|
||||
setSidePanel("diff")
|
||||
} else setSidePanel((prev) => (prev === "diff" ? null : "diff"))
|
||||
setSidePanel(SidePanel.Diff)
|
||||
} else setSidePanel((prev) => (prev === SidePanel.Diff ? null : SidePanel.Diff))
|
||||
} else if (msg.action === "newTab") handleNewTabForCurrentSelection()
|
||||
else if (msg.action === "closeTab") closeActiveTab()
|
||||
else if (msg.action === "newWorktree") showNewWorktreeDialog()
|
||||
@@ -1345,6 +1369,7 @@ const AgentManagerContent: Component = () => {
|
||||
showToast({ variant: "error", title: t("agentManager.terminal.errorTitle"), description: message }),
|
||||
postMessage: (message) => vscode.postMessage(message as never),
|
||||
onCreated: (contextKey, terminalId) => appendToTabOrder(contextKey, terminalId),
|
||||
|
||||
onSideClosed: (_contextKey, terminalId) => forgetTerminalFocus(terminalId),
|
||||
onScriptRunning: (contextKey, terminalId) => {
|
||||
if (terms.sideKey() !== contextKey) return
|
||||
@@ -1412,7 +1437,7 @@ const AgentManagerContent: Component = () => {
|
||||
if (!isActivePayload(ev.projectId)) return
|
||||
// Close diff/review panels — nothing to show during setup.
|
||||
// Terminal panels keep live setup output, so they stay open.
|
||||
if (sidePanel() === "diff") setSidePanel(null)
|
||||
if (sidePanel() === SidePanel.Diff) setSidePanel(null)
|
||||
setReviewActive(false)
|
||||
setSetup({ active: true, message: ev.message, branch: ev.branch, worktreeId: ev.worktreeId })
|
||||
}
|
||||
@@ -1562,6 +1587,15 @@ const AgentManagerContent: Component = () => {
|
||||
managedSession: focusManagedSession,
|
||||
})
|
||||
|
||||
if (msg.type === "agentManager.prError") {
|
||||
const ev = msg as AgentManagerPRErrorMessage
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: t(`agentManager.pr.error.${ev.error}.title`),
|
||||
description: t(`agentManager.pr.error.${ev.error}.description`),
|
||||
})
|
||||
}
|
||||
|
||||
if (projectLive.apply(msg)) return
|
||||
})
|
||||
|
||||
@@ -2011,7 +2045,7 @@ const AgentManagerContent: Component = () => {
|
||||
|
||||
const sideCtl = createSideTerminal({
|
||||
handlers: termHandlers,
|
||||
visible: () => sidePanel() === "terminal" && !history() && !reviewActive(),
|
||||
visible: () => sidePanel() === SidePanel.Terminal && !history() && !reviewActive(),
|
||||
focusedId: () => terms.sideFocusedId(),
|
||||
count: () => terms.sidesForContext(terms.sideKey()).length,
|
||||
isScript: terms.isScript,
|
||||
@@ -2144,7 +2178,7 @@ const AgentManagerContent: Component = () => {
|
||||
const closeActiveTab = () => {
|
||||
// A focused side terminal owns Cmd+W while its panel is visible.
|
||||
// Closing a chat tab out from under the user's cursor would be surprising.
|
||||
if (sidePanel() === "terminal" && terms.sideFocusedId()) {
|
||||
if (sidePanel() === SidePanel.Terminal && terms.sideFocusedId()) {
|
||||
if (sideCtl.close()) return
|
||||
}
|
||||
if (termHandlers.closeFocused()) {
|
||||
@@ -2200,10 +2234,10 @@ const AgentManagerContent: Component = () => {
|
||||
})
|
||||
if (reviewActive()) {
|
||||
closeReviewTab()
|
||||
setSidePanel("diff")
|
||||
setSidePanel(SidePanel.Diff)
|
||||
return
|
||||
}
|
||||
setSidePanel((prev) => (prev === "diff" ? null : "diff"))
|
||||
setSidePanel((prev) => (prev === SidePanel.Diff ? null : SidePanel.Diff))
|
||||
}
|
||||
|
||||
const renderTabById = (id: string) =>
|
||||
@@ -2399,8 +2433,11 @@ const AgentManagerContent: Component = () => {
|
||||
reviewActive={reviewActive}
|
||||
onToggleDiff={toggleDiffPanel}
|
||||
onToggleReview={metrics.click("fullscreen_review", "tab_toolbar", toggleReviewTab)}
|
||||
prStatus={() => activePR()?.pr}
|
||||
prOpen={prOpen}
|
||||
onTogglePR={togglePRPanel}
|
||||
terminalDestination={sideCtl.destination}
|
||||
terminalDestinationActive={() => sidePanel() === "terminal"}
|
||||
terminalDestinationActive={() => sidePanel() === SidePanel.Terminal}
|
||||
terminalKeybind={() => kb().showTerminal ?? ""}
|
||||
onTerminalDestinationOpen={() => {
|
||||
cancelAmbientSetup()
|
||||
@@ -2588,7 +2625,7 @@ const AgentManagerContent: Component = () => {
|
||||
/>
|
||||
</Show>
|
||||
<div class="am-diff-panel-wrapper">
|
||||
<Show when={sidePanel() === "diff"}>
|
||||
<Show when={sidePanel() === SidePanel.Diff}>
|
||||
<DiffPanel
|
||||
diffs={reviewDiffs()}
|
||||
loading={diffLoading()}
|
||||
@@ -2623,10 +2660,29 @@ const AgentManagerContent: Component = () => {
|
||||
activeTerminalId={terms.activeId()}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={sidePanel() === SidePanel.PR && activePR()}>
|
||||
{(() => {
|
||||
const data = activePR()!
|
||||
return (
|
||||
<PRPanel
|
||||
pr={data.pr}
|
||||
worktree={data.wt}
|
||||
onClose={() => setSidePanel(null)}
|
||||
onOpenExternal={() =>
|
||||
vscode.postMessage({
|
||||
type: "agentManager.openPR",
|
||||
worktreeId: data.selected,
|
||||
url: data.pr.url,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</Show>
|
||||
<SideTerminalPanel
|
||||
state={terms}
|
||||
contextKey={terms.sideKey}
|
||||
visible={() => sidePanel() === "terminal"}
|
||||
visible={() => sidePanel() === SidePanel.Terminal}
|
||||
nextKeybind={kb().nextTerminal ?? ""}
|
||||
closeKeybind={kb().closeTab ?? ""}
|
||||
onFocusPrompt={focusPrompt}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
closestCenter,
|
||||
type DragEvent,
|
||||
} from "@thisbeyond/solid-dnd"
|
||||
import type { LocalGitStats, RunStatus, WorktreeGitStats } from "../src/types/messages"
|
||||
import type { LocalGitStats, RunStatus, WorktreeGitStats, PRStatus } from "../src/types/messages"
|
||||
import type { LanguageContextValue } from "../src/context/language"
|
||||
import { LOCAL } from "./navigate"
|
||||
import { ConstrainDragYAxis } from "../src/components/chat/TabDnd"
|
||||
@@ -55,6 +55,9 @@ export interface TabBarProps {
|
||||
reviewActive: () => boolean
|
||||
onToggleDiff: () => void
|
||||
onToggleReview: () => void
|
||||
prStatus: () => PRStatus | undefined
|
||||
prOpen: () => boolean
|
||||
onTogglePR: () => void
|
||||
terminalDestination: () => TerminalDestination
|
||||
terminalDestinationActive: () => boolean
|
||||
terminalKeybind: () => string
|
||||
@@ -215,6 +218,20 @@ export const TabBar: Component<TabBarProps> = (props) => (
|
||||
)
|
||||
})()}
|
||||
</Show>
|
||||
<Show when={props.prStatus()}>
|
||||
{(pr) => (
|
||||
<Tooltip value={`PR #${pr().number}`} placement="bottom">
|
||||
<IconButton
|
||||
icon="pull-request"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={`PR #${pr().number}`}
|
||||
class={props.prOpen() ? "am-tab-diff-btn-active" : ""}
|
||||
onClick={props.onTogglePR}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
title={props.t("agentManager.diff.toggle")}
|
||||
keybind={props.bindings().toggleDiff ?? ""}
|
||||
@@ -226,14 +243,19 @@ export const TabBar: Component<TabBarProps> = (props) => (
|
||||
title={props.t("agentManager.diff.toggle")}
|
||||
>
|
||||
<Icon name="layers" size="small" />
|
||||
<Show when={hasChanges()}>
|
||||
<span class="am-diff-toggle-stats">
|
||||
<Show when={stats()!.files > 0}>
|
||||
<span class="am-stat-files">{stats()!.files}f</span>
|
||||
<Show when={props.prStatus()}>
|
||||
{(pr) => (
|
||||
<Show when={pr().additions > 0 || pr().deletions > 0}>
|
||||
<span class="am-diff-toggle-stats">
|
||||
<Show when={pr().additions > 0}>
|
||||
<span class="am-stat-additions">+{pr().additions}</span>
|
||||
</Show>
|
||||
<Show when={pr().deletions > 0}>
|
||||
<span class="am-stat-deletions">−{pr().deletions}</span>
|
||||
</Show>
|
||||
</span>
|
||||
</Show>
|
||||
<span class="am-stat-additions">+{stats()!.additions}</span>
|
||||
<span class="am-stat-deletions">−{stats()!.deletions}</span>
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</button>
|
||||
</TooltipKeybind>
|
||||
|
||||
@@ -10,7 +10,8 @@ import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip"
|
||||
import { HoverCard } from "@kilocode/kilo-ui/hover-card"
|
||||
import { ContextMenu } from "@kilocode/kilo-ui/context-menu"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import type { WorktreeState, WorktreeGitStats, PRStatus, SectionState, RunStatus } from "../src/types/messages"
|
||||
import type { WorktreeState, WorktreeGitStats, SectionState, RunStatus } from "../src/types/messages"
|
||||
import type { PRStatus } from "../src/types/messages"
|
||||
import { colorCss } from "./section-colors"
|
||||
import { useLanguage } from "../src/context/language"
|
||||
import { formatRelativeDate } from "../src/utils/date"
|
||||
@@ -57,7 +58,7 @@ interface WorktreeItemProps {
|
||||
/** Keybinding string for the open-in-vscode action. */
|
||||
openKeybind: string
|
||||
/** PR status for this worktree's branch, or null if no PR. */
|
||||
pr?: PRStatus | null
|
||||
pr?: PRStatus
|
||||
runStatus?: RunStatus
|
||||
/** Callback when the PR badge is clicked. */
|
||||
onOpenPR?: () => void
|
||||
@@ -94,14 +95,6 @@ const hasStats = (s: WorktreeGitStats | undefined): s is WorktreeGitStats =>
|
||||
* and review results are conveyed by a separate status icon (see prBadgeIndicator)
|
||||
* so a failing check is not mistaken for a closed PR.
|
||||
*/
|
||||
export function prAccentColor(pr: PRStatus): string {
|
||||
if (pr.state === "draft") return "var(--text-weaker)"
|
||||
if (pr.state === "merged") return "#a78bfa"
|
||||
if (pr.state === "closed") return "#f87171"
|
||||
if (pr.checks.status === "pending") return "#fbbf24"
|
||||
return "#34d399"
|
||||
}
|
||||
|
||||
/** True while an open PR's checks are still running — drives the pulsing amber badge. */
|
||||
export function prChecksRunning(pr: PRStatus): boolean {
|
||||
return pr.state === "open" && pr.checks.status === "pending"
|
||||
@@ -349,18 +342,30 @@ export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
|
||||
}
|
||||
>
|
||||
{(pr) => {
|
||||
const accent = () => prAccentColor(pr())
|
||||
const indicator = () => prBadgeIndicator(pr())
|
||||
return (
|
||||
<span
|
||||
class="am-pr-badge"
|
||||
style={{ "--pr-accent": accent() }}
|
||||
data-pending={prChecksRunning(pr()) ? "" : undefined}
|
||||
classList={{
|
||||
"am-pr-accent-draft": pr().state === "draft",
|
||||
"am-pr-accent-merged": pr().state === "merged",
|
||||
"am-pr-accent-closed": pr().state === "closed",
|
||||
"am-pr-accent-pending": pr().state === "open" && pr().checks.status === "pending",
|
||||
"am-pr-accent-open": pr().state === "open" && pr().checks.status !== "pending",
|
||||
"am-pr-badge-pending": prChecksRunning(pr()),
|
||||
}}
|
||||
onClick={handleOpenPR}
|
||||
>
|
||||
<Switch fallback={<Icon name="branch" size="small" />}>
|
||||
<Icon name="pull-request" size="small" />
|
||||
<span class="am-pr-badge-number">#{pr().number}</span>
|
||||
<Switch>
|
||||
<Match when={indicator() === "failure"}>
|
||||
<Icon name="circle-x" size="small" class="am-pr-badge-status" data-status="failure" />
|
||||
<Icon
|
||||
name="circle-x-outline"
|
||||
size="small"
|
||||
class="am-pr-badge-status"
|
||||
data-status="failure"
|
||||
/>
|
||||
</Match>
|
||||
<Match when={indicator() === "changes"}>
|
||||
<Icon name="warning" size="small" class="am-pr-badge-status" data-status="changes" />
|
||||
@@ -374,7 +379,6 @@ export const WorktreeItem: Component<WorktreeItemProps> = (props) => {
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<span class="am-pr-badge-number">#{pr().number}</span>
|
||||
</span>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -840,7 +840,7 @@ html[data-theme="kilo-vscode"]
|
||||
}
|
||||
|
||||
.am-run-badge[data-run-state="stopping"] {
|
||||
--am-run-accent: #fbbf24;
|
||||
--am-run-accent: var(--vscode-charts-yellow, #fbbf24);
|
||||
}
|
||||
|
||||
.am-run-badge[data-run-state="success"] {
|
||||
@@ -848,7 +848,7 @@ html[data-theme="kilo-vscode"]
|
||||
}
|
||||
|
||||
.am-run-badge[data-run-state="error"] {
|
||||
--am-run-accent: #f87171;
|
||||
--am-run-accent: var(--vscode-testing-iconFailed, #f87171);
|
||||
}
|
||||
|
||||
@keyframes am-run-pulse {
|
||||
@@ -898,24 +898,44 @@ html[data-theme="kilo-vscode"]
|
||||
}
|
||||
|
||||
.am-stat-additions {
|
||||
color: #34d399;
|
||||
color: var(--vscode-testing-iconPassed, #34d399);
|
||||
}
|
||||
|
||||
.am-stat-deletions {
|
||||
color: #f87171;
|
||||
color: var(--vscode-testing-iconFailed, #f87171);
|
||||
}
|
||||
|
||||
/* PR badge accent colors via classList */
|
||||
.am-pr-accent-open,
|
||||
.am-pr-panel-badge.am-pr-accent-open {
|
||||
--am-pr-accent: var(--vscode-testing-iconPassed, #34d399);
|
||||
}
|
||||
.am-pr-accent-pending,
|
||||
.am-pr-panel-badge.am-pr-accent-pending {
|
||||
--am-pr-accent: var(--vscode-charts-yellow, #fbbf24);
|
||||
}
|
||||
.am-pr-accent-draft,
|
||||
.am-pr-panel-badge.am-pr-accent-draft {
|
||||
--am-pr-accent: var(--text-weaker);
|
||||
}
|
||||
.am-pr-accent-merged,
|
||||
.am-pr-panel-badge.am-pr-accent-merged {
|
||||
--am-pr-accent: var(--vscode-charts-purple, var(--vscode-foreground));
|
||||
}
|
||||
.am-pr-accent-closed,
|
||||
.am-pr-panel-badge.am-pr-accent-closed {
|
||||
--am-pr-accent: var(--vscode-testing-iconFailed, #f87171);
|
||||
}
|
||||
|
||||
/* PR pill badge — second row inside am-wt-row2.
|
||||
Uses --pr-accent custom property (set inline) to derive all colors. */
|
||||
.am-pr-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px 7px 2px 4px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: color-mix(in srgb, var(--pr-accent) 12%, transparent);
|
||||
color: var(--pr-accent);
|
||||
background: color-mix(in srgb, var(--am-pr-accent) 12%, transparent);
|
||||
color: var(--am-pr-accent);
|
||||
cursor: pointer;
|
||||
font-size: var(--kilo-font-size-11);
|
||||
font-weight: 500;
|
||||
@@ -924,34 +944,34 @@ html[data-theme="kilo-vscode"]
|
||||
margin-left: auto;
|
||||
}
|
||||
.am-pr-badge:hover {
|
||||
background: color-mix(in srgb, var(--pr-accent) 25%, transparent);
|
||||
background: color-mix(in srgb, var(--am-pr-accent) 25%, transparent);
|
||||
}
|
||||
.am-pr-badge [data-component="icon"] {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: var(--pr-accent);
|
||||
color: var(--am-pr-accent);
|
||||
}
|
||||
.am-pr-badge-number {
|
||||
line-height: 1;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.am-pr-badge:hover .am-pr-badge-number {
|
||||
color: var(--pr-accent);
|
||||
color: var(--am-pr-accent);
|
||||
}
|
||||
/* Status icon overlaid on the badge — colored by CI/review result, independent
|
||||
of the badge's PR-state accent color. */
|
||||
.am-pr-badge-status[data-status="failure"] {
|
||||
color: #ef4444;
|
||||
color: var(--vscode-testing-iconFailed, #f87171);
|
||||
}
|
||||
.am-pr-badge-status[data-status="changes"] {
|
||||
color: #fbbf24;
|
||||
color: var(--vscode-charts-yellow, #fbbf24);
|
||||
}
|
||||
.am-pr-badge-status[data-status="approved"] {
|
||||
color: #34d399;
|
||||
color: var(--vscode-testing-iconPassed, #34d399);
|
||||
}
|
||||
|
||||
/* Checks still running — pulse the whole amber badge. */
|
||||
.am-pr-badge[data-pending] {
|
||||
.am-pr-badge.am-pr-badge-pending {
|
||||
animation: am-pr-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes am-pr-pulse {
|
||||
@@ -965,17 +985,17 @@ html[data-theme="kilo-vscode"]
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.am-pr-badge[data-pending] {
|
||||
.am-pr-badge.am-pr-badge-pending {
|
||||
animation: none;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.am-worktree-item-active .am-pr-badge {
|
||||
background: color-mix(in srgb, var(--pr-accent) 18%, transparent);
|
||||
background: color-mix(in srgb, var(--am-pr-accent) 18%, transparent);
|
||||
}
|
||||
.am-worktree-item-active .am-pr-badge:hover {
|
||||
background: color-mix(in srgb, var(--pr-accent) 30%, transparent);
|
||||
background: color-mix(in srgb, var(--am-pr-accent) 30%, transparent);
|
||||
}
|
||||
|
||||
/* Clickable link icon in hover card to open PR on GitHub */
|
||||
@@ -996,35 +1016,35 @@ html[data-theme="kilo-vscode"]
|
||||
}
|
||||
|
||||
.am-worktree-item-active .am-stat-files {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
.am-worktree-item-active .am-stat-additions {
|
||||
color: #6ee7b7;
|
||||
color: var(--vscode-testing-iconPassed, #6ee7b7);
|
||||
}
|
||||
|
||||
.am-worktree-item-active .am-stat-deletions {
|
||||
color: #fca5a5;
|
||||
color: var(--vscode-testing-iconFailed, #fca5a5);
|
||||
}
|
||||
|
||||
.am-worktree-commits {
|
||||
color: #34d399;
|
||||
color: var(--vscode-testing-iconPassed, #34d399);
|
||||
}
|
||||
|
||||
.am-worktree-behind {
|
||||
color: #f87171;
|
||||
color: var(--vscode-testing-iconFailed, #f87171);
|
||||
}
|
||||
|
||||
.am-worktree-item-active .am-worktree-commits {
|
||||
color: #6ee7b7;
|
||||
color: var(--vscode-testing-iconPassed, #6ee7b7);
|
||||
}
|
||||
|
||||
.am-worktree-item-active .am-worktree-behind {
|
||||
color: #fca5a5;
|
||||
color: var(--vscode-testing-iconFailed, #fca5a5);
|
||||
}
|
||||
|
||||
.am-local-item-active .am-worktree-behind {
|
||||
color: #fca5a5;
|
||||
color: var(--vscode-testing-iconFailed, #fca5a5);
|
||||
}
|
||||
|
||||
.am-local-item:hover .am-worktree-stats,
|
||||
@@ -1699,7 +1719,7 @@ body.am-wt-dragging-active * {
|
||||
}
|
||||
|
||||
.am-sidebar-search-status {
|
||||
color: #fbbf24;
|
||||
color: var(--vscode-charts-yellow, #fbbf24);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@@ -1794,9 +1814,10 @@ body.am-wt-dragging-active * {
|
||||
color: var(--text-base);
|
||||
}
|
||||
|
||||
.am-diff-toggle-btn.am-tab-diff-btn-active {
|
||||
background: var(--surface-interactive-base) !important;
|
||||
color: var(--text-base) !important;
|
||||
.am-diff-toggle-btn.am-tab-diff-btn-active,
|
||||
.am-diff-toggle-btn.am-tab-diff-btn-active:hover {
|
||||
background: var(--surface-interactive-base);
|
||||
color: var(--text-on-interactive-base);
|
||||
}
|
||||
|
||||
.am-diff-toggle-btn.am-diff-toggle-has-changes {
|
||||
@@ -1810,20 +1831,24 @@ body.am-wt-dragging-active * {
|
||||
}
|
||||
|
||||
.am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-files {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
.am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-additions {
|
||||
color: #6ee7b7;
|
||||
color: var(--vscode-testing-iconPassed, #6ee7b7);
|
||||
}
|
||||
|
||||
.am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-deletions {
|
||||
color: #fca5a5;
|
||||
color: var(--vscode-testing-iconFailed, #fca5a5);
|
||||
}
|
||||
|
||||
.am-tab-diff-btn-active {
|
||||
background: var(--surface-interactive-base) !important;
|
||||
color: var(--text-base) !important;
|
||||
color: var(--text-on-interactive-base) !important;
|
||||
}
|
||||
|
||||
.am-tab-diff-btn-active [data-slot="icon-svg"] {
|
||||
color: var(--text-on-interactive-base) !important;
|
||||
}
|
||||
|
||||
.am-detail-content {
|
||||
@@ -2947,9 +2972,10 @@ body.am-wt-dragging-active * {
|
||||
color: var(--text-base);
|
||||
}
|
||||
|
||||
.am-nv-pill-active {
|
||||
background: var(--border-focus, #007fd4) !important;
|
||||
color: #fff !important;
|
||||
.am-nv-pill-active,
|
||||
.am-nv-pill-active:hover {
|
||||
background: var(--border-focus, #007fd4);
|
||||
color: var(--text-on-interactive-base);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Worktree جديد",
|
||||
"agentManager.worktree.setupScript": "سكربت إعداد Worktree",
|
||||
"agentManager.worktree.delete": "حذف Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "حذف؟",
|
||||
"agentManager.worktree.stale": "قديم",
|
||||
"agentManager.worktree.staleTooltip": "مفقود على القرص أو لم يعد git worktree يتتبعه",
|
||||
"agentManager.worktree.removeStale": "إزالة Worktree القديم",
|
||||
@@ -104,8 +104,8 @@ export const dict = {
|
||||
"agentManager.run.options": "خيارات التشغيل",
|
||||
"agentManager.run.configure": "تكوين سكربت التشغيل",
|
||||
"agentManager.shortcuts.openAgentManager": "فتح Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "وضع الوكيل التالي",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "وضع الوكيل السابق",
|
||||
"agentManager.shortcuts.showShortcuts": "إظهار اختصارات لوحة المفاتيح",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "إزالة Worktree القديم",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "إزالة Worktree القديم ",
|
||||
@@ -211,4 +211,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "لم يتم العثور على فروع.",
|
||||
"agentManager.import.noBranchesHint": "الصق رابط PR أعلاه أو أنشئ Worktree جديدًا.",
|
||||
"agentManager.import.failed": "فشل الاستيراد",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "مصادقة GitHub مطلوبة",
|
||||
"agentManager.pr.error.gh_auth.description": "شغّل 'gh auth login' في الطرفية لاستعادة حالة PR.",
|
||||
"agentManager.pr.error.gh_missing.title": "لم يتم تثبيت GitHub CLI",
|
||||
"agentManager.pr.error.gh_missing.description": "قم بتثبيت GitHub CLI لتفعيل حالة PR.",
|
||||
}
|
||||
|
||||
+8
-3
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Novo Worktree",
|
||||
"agentManager.worktree.setupScript": "Script de configuração do Worktree",
|
||||
"agentManager.worktree.delete": "Excluir Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "Excluir?",
|
||||
"agentManager.worktree.stale": "Obsoleto",
|
||||
"agentManager.worktree.staleTooltip": "Ausente no disco ou não é mais rastreado pelo git worktree",
|
||||
"agentManager.worktree.removeStale": "Remover Worktree obsoleto",
|
||||
@@ -106,8 +106,8 @@ export const dict = {
|
||||
"agentManager.run.options": "Opções de execução",
|
||||
"agentManager.run.configure": "Configurar script de execução",
|
||||
"agentManager.shortcuts.openAgentManager": "Abrir Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Próximo modo de agente",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Modo de agente anterior",
|
||||
"agentManager.shortcuts.showShortcuts": "Mostrar atalhos de teclado",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "Remover Worktree obsoleto",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "Remover Worktree obsoleto ",
|
||||
@@ -215,4 +215,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Nenhum branch encontrado.",
|
||||
"agentManager.import.noBranchesHint": "Cole uma URL de PR acima ou crie um novo Worktree.",
|
||||
"agentManager.import.failed": "Falha na importação",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "Autenticação do GitHub necessária",
|
||||
"agentManager.pr.error.gh_auth.description": "Execute 'gh auth login' no terminal para restaurar o status do PR.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI não instalado",
|
||||
"agentManager.pr.error.gh_missing.description": "Instale o GitHub CLI para habilitar o status do PR.",
|
||||
}
|
||||
|
||||
+9
-4
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Novi Worktree",
|
||||
"agentManager.worktree.setupScript": "Skripta za postavljanje Worktree-a",
|
||||
"agentManager.worktree.delete": "Obriši Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "Izbrisati?",
|
||||
"agentManager.worktree.stale": "Zastario",
|
||||
"agentManager.worktree.staleTooltip": "Nedostaje na disku ili ga git worktree više ne prati",
|
||||
"agentManager.worktree.removeStale": "Ukloni zastarjeli Worktree",
|
||||
@@ -106,8 +106,8 @@ export const dict = {
|
||||
"agentManager.run.options": "Opcije pokretanja",
|
||||
"agentManager.run.configure": "Konfiguriši skriptu za pokretanje",
|
||||
"agentManager.shortcuts.openAgentManager": "Otvori Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Sljedeći način rada agenta",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Prethodni način rada agenta",
|
||||
"agentManager.shortcuts.showShortcuts": "Prikaži prečice na tastaturi",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "Ukloni zastarjeli Worktree",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "Ukloni zastarjeli Worktree ",
|
||||
@@ -203,7 +203,7 @@ export const dict = {
|
||||
"agentManager.review.imageUnavailable": "Pregled slike nije dostupan za ovaj snimak sesije.",
|
||||
"agentManager.review.endOfLongDiff": "Došli ste do kraja!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "Zahtjev za povlačenje",
|
||||
"agentManager.import.pastePrUrl": "Zalijepite PR URL...",
|
||||
"agentManager.import.open": "Otvori",
|
||||
"agentManager.import.branches": "Branchevi",
|
||||
@@ -214,4 +214,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Nisu pronađeni branchevi.",
|
||||
"agentManager.import.noBranchesHint": "Zalijepite PR URL iznad ili kreirajte novi Worktree.",
|
||||
"agentManager.import.failed": "Uvoz neuspješan",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "Potrebna GitHub autentikacija",
|
||||
"agentManager.pr.error.gh_auth.description": "Pokrenite 'gh auth login' u terminalu da vratite status PR-a.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI nije instaliran",
|
||||
"agentManager.pr.error.gh_missing.description": "Instalirajte GitHub CLI da omogućite status PR-a.",
|
||||
}
|
||||
|
||||
+10
-5
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Nyt Worktree",
|
||||
"agentManager.worktree.setupScript": "Worktree-opsætningsscript",
|
||||
"agentManager.worktree.delete": "Slet Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "Slet?",
|
||||
"agentManager.worktree.stale": "Forældet",
|
||||
"agentManager.worktree.staleTooltip": "Mangler på disken eller spores ikke længere af git worktree",
|
||||
"agentManager.worktree.removeStale": "Fjern forældet Worktree",
|
||||
@@ -107,8 +107,8 @@ export const dict = {
|
||||
"agentManager.run.options": "Kørselsindstillinger",
|
||||
"agentManager.run.configure": "Konfigurer kørselsscript",
|
||||
"agentManager.shortcuts.openAgentManager": "Åbn Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Næste agenttilstand",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Forrige agenttilstand",
|
||||
"agentManager.shortcuts.showShortcuts": "Vis tastaturgenveje",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "Fjern forældet Worktree",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "Fjern forældet Worktree ",
|
||||
@@ -205,10 +205,10 @@ export const dict = {
|
||||
"Forhåndsvisning af billedet er ikke tilgængelig for dette snapshot af sessionen.",
|
||||
"agentManager.review.endOfLongDiff": "Du nåede slutningen!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "Pull request",
|
||||
"agentManager.import.pastePrUrl": "Indsæt PR URL...",
|
||||
"agentManager.import.open": "Åbn",
|
||||
"agentManager.import.branches": "Branches",
|
||||
"agentManager.import.branches": "Grene",
|
||||
"agentManager.import.selectBranch": "Vælg branch...",
|
||||
"agentManager.import.loading": "Indlæser...",
|
||||
"agentManager.import.loadingBranches": "Indlæser branches...",
|
||||
@@ -216,4 +216,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Ingen branches fundet.",
|
||||
"agentManager.import.noBranchesHint": "Indsæt en PR URL ovenfor eller opret et nyt Worktree.",
|
||||
"agentManager.import.failed": "Import mislykkedes",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "GitHub-godkendelse påkrævet",
|
||||
"agentManager.pr.error.gh_auth.description": "Kør 'gh auth login' i din terminal for at gendanne PR-status.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI er ikke installeret",
|
||||
"agentManager.pr.error.gh_missing.description": "Installer GitHub CLI for at aktivere PR-status.",
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Neuer Worktree",
|
||||
"agentManager.worktree.setupScript": "Worktree-Einrichtungsskript",
|
||||
"agentManager.worktree.delete": "Worktree löschen",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "Löschen?",
|
||||
"agentManager.worktree.stale": "Veraltet",
|
||||
"agentManager.worktree.staleTooltip": "Fehlt auf der Festplatte oder wird nicht mehr von git worktree verfolgt",
|
||||
"agentManager.worktree.removeStale": "Veralteten Worktree entfernen",
|
||||
@@ -108,8 +108,8 @@ export const dict = {
|
||||
"agentManager.run.options": "Ausführungsoptionen",
|
||||
"agentManager.run.configure": "Ausführungsskript konfigurieren",
|
||||
"agentManager.shortcuts.openAgentManager": "Agent Manager öffnen",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Nächster Agentenmodus",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Vorheriger Agentenmodus",
|
||||
"agentManager.shortcuts.showShortcuts": "Tastenkürzel anzeigen",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "Veralteten Worktree entfernen",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "Veralteten Worktree entfernen ",
|
||||
@@ -208,7 +208,7 @@ export const dict = {
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "PR-URL einfügen...",
|
||||
"agentManager.import.open": "Öffnen",
|
||||
"agentManager.import.branches": "Branches",
|
||||
"agentManager.import.branches": "Zweige",
|
||||
"agentManager.import.selectBranch": "Branch auswählen...",
|
||||
"agentManager.import.loading": "Laden...",
|
||||
"agentManager.import.loadingBranches": "Branches werden geladen...",
|
||||
@@ -216,4 +216,10 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Keine Branches gefunden.",
|
||||
"agentManager.import.noBranchesHint": "Fügen Sie oben eine PR-URL ein oder erstellen Sie einen neuen Worktree.",
|
||||
"agentManager.import.failed": "Import fehlgeschlagen",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "GitHub-Authentifizierung erforderlich",
|
||||
"agentManager.pr.error.gh_auth.description":
|
||||
"Führen Sie 'gh auth login' in Ihrem Terminal aus, um den PR-Status wiederherzustellen.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI nicht installiert",
|
||||
"agentManager.pr.error.gh_missing.description": "Installieren Sie die GitHub CLI, um den PR-Status zu aktivieren.",
|
||||
}
|
||||
|
||||
@@ -217,4 +217,8 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "No branches found.",
|
||||
"agentManager.import.noBranchesHint": "Paste a PR URL above or create a new worktree.",
|
||||
"agentManager.import.failed": "Import failed",
|
||||
"agentManager.pr.error.gh_auth.title": "GitHub authentication required",
|
||||
"agentManager.pr.error.gh_auth.description": "Run 'gh auth login' in your terminal to restore PR status.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI not installed",
|
||||
"agentManager.pr.error.gh_missing.description": "Install the GitHub CLI to enable PR status.",
|
||||
}
|
||||
|
||||
+10
-4
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Nuevo Worktree",
|
||||
"agentManager.worktree.setupScript": "Script de configuración de Worktree",
|
||||
"agentManager.worktree.delete": "Eliminar Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "¿Eliminar?",
|
||||
"agentManager.worktree.stale": "Obsoleto",
|
||||
"agentManager.worktree.staleTooltip": "No está en disco o ya no está seguido por git worktree",
|
||||
"agentManager.worktree.removeStale": "Eliminar Worktree obsoleto",
|
||||
@@ -107,8 +107,8 @@ export const dict = {
|
||||
"agentManager.run.options": "Opciones de ejecución",
|
||||
"agentManager.run.configure": "Configurar script de ejecución",
|
||||
"agentManager.shortcuts.openAgentManager": "Abrir Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Siguiente modo de agente",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Modo de agente anterior",
|
||||
"agentManager.shortcuts.showShortcuts": "Mostrar atajos de teclado",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "Eliminar Worktree obsoleto",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "¿Eliminar Worktree obsoleto ",
|
||||
@@ -208,7 +208,7 @@ export const dict = {
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pastePrUrl": "Pegar URL del PR...",
|
||||
"agentManager.import.open": "Abrir",
|
||||
"agentManager.import.branches": "Branches",
|
||||
"agentManager.import.branches": "Ramas",
|
||||
"agentManager.import.selectBranch": "Seleccionar branch...",
|
||||
"agentManager.import.loading": "Cargando...",
|
||||
"agentManager.import.loadingBranches": "Cargando branches...",
|
||||
@@ -216,4 +216,10 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "No se encontraron branches.",
|
||||
"agentManager.import.noBranchesHint": "Pega una URL de PR arriba o crea un nuevo Worktree.",
|
||||
"agentManager.import.failed": "Error en la importación",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "Se requiere autenticación de GitHub",
|
||||
"agentManager.pr.error.gh_auth.description":
|
||||
"Ejecuta 'gh auth login' en tu terminal para restaurar el estado del PR.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI no está instalado",
|
||||
"agentManager.pr.error.gh_missing.description": "Instala GitHub CLI para habilitar el estado del PR.",
|
||||
}
|
||||
|
||||
+8
-3
@@ -3,8 +3,8 @@ export const dict = {
|
||||
"agentManager.sidebar.collapse": "بستن نوار کناری",
|
||||
"agentManager.sidebar.expand": "نمایش نوار کناری",
|
||||
"agentManager.section.worktrees": "WORKTREES",
|
||||
"agentManager.section.sessions": "SESSIONS",
|
||||
"agentManager.projects": "PROJECTS",
|
||||
"agentManager.section.sessions": "جلسات",
|
||||
"agentManager.projects": "پروژهها",
|
||||
"agentManager.project.add": "افزودن پروژه",
|
||||
"agentManager.project.remove": "حذف از Agent Manager",
|
||||
"agentManager.project.trust": "اعتماد",
|
||||
@@ -207,7 +207,7 @@ export const dict = {
|
||||
"agentManager.review.imageUnavailable": "پیشنمایش تصویر برای این نمونه جلسه در دسترس نیست.",
|
||||
"agentManager.review.endOfLongDiff": "به انتها رسیدید!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "درخواست ادغام",
|
||||
"agentManager.import.pastePrUrl": "URL درخواست PR را وارد کنید...",
|
||||
"agentManager.import.open": "باز کردن",
|
||||
"agentManager.import.branches": "شاخهها",
|
||||
@@ -218,4 +218,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "هیچ شاخهای یافت نشد.",
|
||||
"agentManager.import.noBranchesHint": "یک URL درخواست ادغام را در بالا جایگذاری کنید یا یک worktree جدید بسازید.",
|
||||
"agentManager.import.failed": "وارد کردن ناموفق بود",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "احراز هویت GitHub لازم است",
|
||||
"agentManager.pr.error.gh_auth.description": "برای بازیابی وضعیت PR، دستور 'gh auth login' را در ترمینال اجرا کنید.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI نصب نشده است",
|
||||
"agentManager.pr.error.gh_missing.description": "برای فعال کردن وضعیت PR، GitHub CLI را نصب کنید.",
|
||||
}
|
||||
|
||||
+10
-4
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Nouveau Worktree",
|
||||
"agentManager.worktree.setupScript": "Script de configuration du Worktree",
|
||||
"agentManager.worktree.delete": "Supprimer le Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "Supprimer ?",
|
||||
"agentManager.worktree.stale": "Obsolète",
|
||||
"agentManager.worktree.staleTooltip": "Absent du disque ou n'est plus suivi par git worktree",
|
||||
"agentManager.worktree.removeStale": "Supprimer le Worktree obsolète",
|
||||
@@ -60,7 +60,7 @@ export const dict = {
|
||||
"agentManager.sidebarSearch.label": "Rechercher des Worktrees et des sessions",
|
||||
"agentManager.sidebarSearch.scope":
|
||||
"Recherche dans l'espace de travail local, les sessions locales, les Worktrees et leurs sessions",
|
||||
"agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES",
|
||||
"agentManager.sidebarSearch.contexts": "LOCAL ET WORKTREES",
|
||||
|
||||
"agentManager.terminal.new": "Nouvel onglet de terminal",
|
||||
"agentManager.terminal.ended": "terminal terminé — fermez l'onglet pour ignorer",
|
||||
@@ -107,8 +107,8 @@ export const dict = {
|
||||
"agentManager.run.options": "Options d'exécution",
|
||||
"agentManager.run.configure": "Configurer le script d'exécution",
|
||||
"agentManager.shortcuts.openAgentManager": "Ouvrir Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Mode d'agent suivant",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Mode d'agent précédent",
|
||||
"agentManager.shortcuts.showShortcuts": "Afficher les raccourcis clavier",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "Supprimer le Worktree obsolète",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "Supprimer le Worktree obsolète ",
|
||||
@@ -216,4 +216,10 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Aucune branche trouvée.",
|
||||
"agentManager.import.noBranchesHint": "Collez une URL de PR ci-dessus ou créez un nouveau Worktree.",
|
||||
"agentManager.import.failed": "Échec de l'importation",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "Authentification GitHub requise",
|
||||
"agentManager.pr.error.gh_auth.description":
|
||||
"Exécutez 'gh auth login' dans votre terminal pour restaurer le statut de la PR.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI non installé",
|
||||
"agentManager.pr.error.gh_missing.description": "Installez GitHub CLI pour activer le statut de la PR.",
|
||||
}
|
||||
|
||||
+7
-1
@@ -36,7 +36,7 @@ export const dict = {
|
||||
"agentManager.section.moveUp": "Sposta su",
|
||||
"agentManager.section.moveDown": "Sposta giù",
|
||||
|
||||
"agentManager.hoverCard.branch": "BRANCH",
|
||||
"agentManager.hoverCard.branch": "Ramo",
|
||||
"agentManager.hoverCard.worktree": "Worktree",
|
||||
"agentManager.hoverCard.base": "Base",
|
||||
"agentManager.hoverCard.sessions": "Sessioni",
|
||||
@@ -222,4 +222,10 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Nessun branch trovato.",
|
||||
"agentManager.import.noBranchesHint": "Incolla un URL PR sopra o crea un nuovo worktree.",
|
||||
"agentManager.import.failed": "Importazione non riuscita",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "Autenticazione GitHub richiesta",
|
||||
"agentManager.pr.error.gh_auth.description":
|
||||
"Esegui 'gh auth login' nel terminale per ripristinare lo stato della PR.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI non installato",
|
||||
"agentManager.pr.error.gh_missing.description": "Installa GitHub CLI per abilitare lo stato della PR.",
|
||||
}
|
||||
|
||||
+11
-4
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "新しいWorktree",
|
||||
"agentManager.worktree.setupScript": "Worktreeセットアップスクリプト",
|
||||
"agentManager.worktree.delete": "Worktreeを削除",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "削除しますか?",
|
||||
"agentManager.worktree.stale": "無効",
|
||||
"agentManager.worktree.staleTooltip": "ディスク上に存在しないか、git worktree で追跡されていません",
|
||||
"agentManager.worktree.removeStale": "無効な Worktree を削除",
|
||||
@@ -107,8 +107,8 @@ export const dict = {
|
||||
"agentManager.run.options": "実行オプション",
|
||||
"agentManager.run.configure": "実行スクリプトを設定",
|
||||
"agentManager.shortcuts.openAgentManager": "Agent Managerを開く",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "次のエージェントモード",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "前のエージェントモード",
|
||||
"agentManager.shortcuts.showShortcuts": "キーボードショートカットを表示",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "無効な Worktree を削除",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "無効な Worktree を削除 ",
|
||||
@@ -204,7 +204,7 @@ export const dict = {
|
||||
"agentManager.review.imageUnavailable": "このセッションスナップショットでは画像プレビューを利用できません。",
|
||||
"agentManager.review.endOfLongDiff": "最後まで到達しました!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "プルリクエスト",
|
||||
"agentManager.import.pastePrUrl": "PR URLを貼り付け...",
|
||||
"agentManager.import.open": "開く",
|
||||
"agentManager.import.branches": "ブランチ",
|
||||
@@ -215,4 +215,11 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "ブランチが見つかりません。",
|
||||
"agentManager.import.noBranchesHint": "上にPR URLを貼り付けるか、新しいWorktreeを作成してください。",
|
||||
"agentManager.import.failed": "インポートに失敗しました",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "GitHub認証が必要です",
|
||||
"agentManager.pr.error.gh_auth.description":
|
||||
"PRステータスを復元するには、ターミナルで 'gh auth login' を実行してください。",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLIがインストールされていません",
|
||||
"agentManager.pr.error.gh_missing.description":
|
||||
"PRステータスを有効にするには、GitHub CLIをインストールしてください。",
|
||||
}
|
||||
|
||||
+9
-4
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "새 Worktree",
|
||||
"agentManager.worktree.setupScript": "Worktree 설정 스크립트",
|
||||
"agentManager.worktree.delete": "Worktree 삭제",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "삭제하시겠습니까?",
|
||||
"agentManager.worktree.stale": "오래됨",
|
||||
"agentManager.worktree.staleTooltip": "디스크에 없거나 더 이상 git worktree에서 추적되지 않습니다",
|
||||
"agentManager.worktree.removeStale": "오래된 Worktree 제거",
|
||||
@@ -105,8 +105,8 @@ export const dict = {
|
||||
"agentManager.run.options": "실행 옵션",
|
||||
"agentManager.run.configure": "실행 스크립트 구성",
|
||||
"agentManager.shortcuts.openAgentManager": "Agent Manager 열기",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "다음 에이전트 모드",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "이전 에이전트 모드",
|
||||
"agentManager.shortcuts.showShortcuts": "키보드 단축키 표시",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "오래된 Worktree 제거",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "오래된 Worktree 제거 ",
|
||||
@@ -202,7 +202,7 @@ export const dict = {
|
||||
"agentManager.review.imageUnavailable": "이 세션 스냅샷에서는 이미지 미리보기를 사용할 수 없습니다.",
|
||||
"agentManager.review.endOfLongDiff": "끝까지 도달했습니다!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "풀 리퀘스트",
|
||||
"agentManager.import.pastePrUrl": "PR URL 붙여넣기...",
|
||||
"agentManager.import.open": "열기",
|
||||
"agentManager.import.branches": "브랜치",
|
||||
@@ -213,4 +213,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "브랜치를 찾을 수 없습니다.",
|
||||
"agentManager.import.noBranchesHint": "위에 PR URL을 붙여넣거나 새 Worktree를 만드세요.",
|
||||
"agentManager.import.failed": "가져오기 실패",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "GitHub 인증 필요",
|
||||
"agentManager.pr.error.gh_auth.description": "PR 상태를 복원하려면 터미널에서 'gh auth login'을 실행하세요.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI가 설치되지 않음",
|
||||
"agentManager.pr.error.gh_missing.description": "PR 상태를 활성화하려면 GitHub CLI를 설치하세요.",
|
||||
}
|
||||
|
||||
+7
-2
@@ -138,7 +138,7 @@ export const dict = {
|
||||
"agentManager.dialog.baseBranch": "Basis branch",
|
||||
"agentManager.dialog.searchBranches": "Zoeken naar branches...",
|
||||
"agentManager.dialog.branchBadge.default": "standaard",
|
||||
"agentManager.dialog.branchBadge.remote": "remote",
|
||||
"agentManager.dialog.branchBadge.remote": "extern",
|
||||
"agentManager.dialog.versions": "Versies",
|
||||
"agentManager.dialog.versionHint": "{{count}} worktrees zullen parallel draaien",
|
||||
"agentManager.dialog.compareModels": "Modellen vergelijken",
|
||||
@@ -209,7 +209,7 @@ export const dict = {
|
||||
"agentManager.review.imageUnavailable": "Afbeeldingsvoorbeeld is niet beschikbaar voor deze sessiesnapshot.",
|
||||
"agentManager.review.endOfLongDiff": "Je hebt het einde bereikt!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "Pull request",
|
||||
"agentManager.import.pastePrUrl": "Plak PR URL...",
|
||||
"agentManager.import.open": "Openen",
|
||||
"agentManager.import.branches": "Branches",
|
||||
@@ -220,4 +220,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Geen branches gevonden.",
|
||||
"agentManager.import.noBranchesHint": "Plak hierboven een PR URL of maak een nieuwe worktree.",
|
||||
"agentManager.import.failed": "Importeren mislukt",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "GitHub-authenticatie vereist",
|
||||
"agentManager.pr.error.gh_auth.description": "Voer 'gh auth login' uit in je terminal om de PR-status te herstellen.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI niet geïnstalleerd",
|
||||
"agentManager.pr.error.gh_missing.description": "Installeer de GitHub CLI om de PR-status in te schakelen.",
|
||||
}
|
||||
|
||||
+10
-5
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Nytt Worktree",
|
||||
"agentManager.worktree.setupScript": "Worktree-oppsettskript",
|
||||
"agentManager.worktree.delete": "Slett Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "Slette?",
|
||||
"agentManager.worktree.stale": "Utdatert",
|
||||
"agentManager.worktree.staleTooltip": "Mangler på disken eller spores ikke lenger av git worktree",
|
||||
"agentManager.worktree.removeStale": "Fjern utdatert Worktree",
|
||||
@@ -105,8 +105,8 @@ export const dict = {
|
||||
"agentManager.run.options": "Kjøringsalternativer",
|
||||
"agentManager.run.configure": "Konfigurer kjøreskript",
|
||||
"agentManager.shortcuts.openAgentManager": "Åpne Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Neste agentmodus",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Forrige agentmodus",
|
||||
"agentManager.shortcuts.showShortcuts": "Vis tastatursnarveier",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "Fjern utdatert Worktree",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "Fjern utdatert Worktree ",
|
||||
@@ -202,10 +202,10 @@ export const dict = {
|
||||
"Forhåndsvisning av bildet er ikke tilgjengelig for dette øyeblikksbildet av økten.",
|
||||
"agentManager.review.endOfLongDiff": "Du nådde slutten!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "Pull request",
|
||||
"agentManager.import.pastePrUrl": "Lim inn PR URL...",
|
||||
"agentManager.import.open": "Åpne",
|
||||
"agentManager.import.branches": "Branches",
|
||||
"agentManager.import.branches": "Grener",
|
||||
"agentManager.import.selectBranch": "Velg branch...",
|
||||
"agentManager.import.loading": "Laster...",
|
||||
"agentManager.import.loadingBranches": "Laster branches...",
|
||||
@@ -213,4 +213,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Ingen branches funnet.",
|
||||
"agentManager.import.noBranchesHint": "Lim inn en PR URL ovenfor eller opprett et nytt Worktree.",
|
||||
"agentManager.import.failed": "Import mislyktes",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "GitHub-autentisering kreves",
|
||||
"agentManager.pr.error.gh_auth.description": "Kjør 'gh auth login' i terminalen for å gjenopprette PR-status.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI er ikke installert",
|
||||
"agentManager.pr.error.gh_missing.description": "Installer GitHub CLI for å aktivere PR-status.",
|
||||
}
|
||||
|
||||
+8
-3
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Nowy Worktree",
|
||||
"agentManager.worktree.setupScript": "Skrypt konfiguracji Worktree",
|
||||
"agentManager.worktree.delete": "Usuń Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "Usunąć?",
|
||||
"agentManager.worktree.stale": "Nieaktualny",
|
||||
"agentManager.worktree.staleTooltip": "Brak na dysku lub nie jest już śledzony przez git worktree",
|
||||
"agentManager.worktree.removeStale": "Usuń nieaktualny Worktree",
|
||||
@@ -107,8 +107,8 @@ export const dict = {
|
||||
"agentManager.run.options": "Opcje uruchamiania",
|
||||
"agentManager.run.configure": "Konfiguruj skrypt uruchamiania",
|
||||
"agentManager.shortcuts.openAgentManager": "Otwórz Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Następny tryb agenta",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Poprzedni tryb agenta",
|
||||
"agentManager.shortcuts.showShortcuts": "Pokaż skróty klawiszowe",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "Usuń nieaktualny Worktree",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "Usunąć nieaktualny Worktree ",
|
||||
@@ -214,4 +214,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Nie znaleziono branchy.",
|
||||
"agentManager.import.noBranchesHint": "Wklej URL PR powyżej lub utwórz nowy Worktree.",
|
||||
"agentManager.import.failed": "Import nie powiódł się",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "Wymagana autoryzacja GitHub",
|
||||
"agentManager.pr.error.gh_auth.description": "Uruchom 'gh auth login' w terminalu, aby przywrócić status PR.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI nie jest zainstalowane",
|
||||
"agentManager.pr.error.gh_missing.description": "Zainstaluj GitHub CLI, aby włączyć status PR.",
|
||||
}
|
||||
|
||||
+8
-3
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Новый Worktree",
|
||||
"agentManager.worktree.setupScript": "Скрипт настройки Worktree",
|
||||
"agentManager.worktree.delete": "Удалить Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "Удалить?",
|
||||
"agentManager.worktree.stale": "Устаревший",
|
||||
"agentManager.worktree.staleTooltip": "Отсутствует на диске или больше не отслеживается git worktree",
|
||||
"agentManager.worktree.removeStale": "Удалить устаревший Worktree",
|
||||
@@ -107,8 +107,8 @@ export const dict = {
|
||||
"agentManager.run.options": "Параметры запуска",
|
||||
"agentManager.run.configure": "Настроить скрипт запуска",
|
||||
"agentManager.shortcuts.openAgentManager": "Открыть Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Следующий режим агента",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Предыдущий режим агента",
|
||||
"agentManager.shortcuts.showShortcuts": "Показать сочетания клавиш",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "Удалить устаревший Worktree",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "Удалить устаревший Worktree ",
|
||||
@@ -215,4 +215,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Ветки не найдены.",
|
||||
"agentManager.import.noBranchesHint": "Вставьте URL PR выше или создайте новый Worktree.",
|
||||
"agentManager.import.failed": "Ошибка импорта",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "Требуется аутентификация GitHub",
|
||||
"agentManager.pr.error.gh_auth.description": "Выполните 'gh auth login' в терминале, чтобы восстановить статус PR.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI не установлен",
|
||||
"agentManager.pr.error.gh_missing.description": "Установите GitHub CLI, чтобы включить статус PR.",
|
||||
}
|
||||
|
||||
+10
-5
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "Worktree ใหม่",
|
||||
"agentManager.worktree.setupScript": "สคริปต์ตั้งค่า Worktree",
|
||||
"agentManager.worktree.delete": "ลบ Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "ลบ?",
|
||||
"agentManager.worktree.stale": "ล้าสมัย",
|
||||
"agentManager.worktree.staleTooltip": "ไม่พบบนดิสก์หรือไม่ได้ถูกติดตามโดย git worktree อีกต่อไป",
|
||||
"agentManager.worktree.removeStale": "ลบ Worktree ที่ล้าสมัย",
|
||||
@@ -102,8 +102,8 @@ export const dict = {
|
||||
"agentManager.run.options": "ตัวเลือกการเรียกใช้",
|
||||
"agentManager.run.configure": "กำหนดค่าสคริปต์การเรียกใช้",
|
||||
"agentManager.shortcuts.openAgentManager": "เปิด Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "โหมดเอเจนต์ถัดไป",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "โหมดเอเจนต์ก่อนหน้า",
|
||||
"agentManager.shortcuts.showShortcuts": "แสดงปุ่มลัดแป้นพิมพ์",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "ลบ Worktree ที่ล้าสมัย",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "ลบ Worktree ที่ล้าสมัย ",
|
||||
@@ -198,10 +198,10 @@ export const dict = {
|
||||
"agentManager.review.imageUnavailable": "ไม่สามารถแสดงตัวอย่างรูปภาพสำหรับสแนปช็อตของเซสชันนี้ได้",
|
||||
"agentManager.review.endOfLongDiff": "คุณมาถึงท้ายสุดแล้ว!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "คำขอรวมโค้ด",
|
||||
"agentManager.import.pastePrUrl": "วาง URL ของ PR...",
|
||||
"agentManager.import.open": "เปิด",
|
||||
"agentManager.import.branches": "Branches",
|
||||
"agentManager.import.branches": "สาขา",
|
||||
"agentManager.import.selectBranch": "เลือก Branch...",
|
||||
"agentManager.import.loading": "กำลังโหลด...",
|
||||
"agentManager.import.loadingBranches": "กำลังโหลด Branches...",
|
||||
@@ -209,4 +209,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "ไม่พบ Branch",
|
||||
"agentManager.import.noBranchesHint": "วาง URL ของ PR ด้านบนหรือสร้าง Worktree ใหม่",
|
||||
"agentManager.import.failed": "นำเข้าล้มเหลว",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "ต้องยืนยันตัวตน GitHub",
|
||||
"agentManager.pr.error.gh_auth.description": "รันคำสั่ง 'gh auth login' ในเทอร์มินัลเพื่อกู้คืนสถานะ PR",
|
||||
"agentManager.pr.error.gh_missing.title": "ไม่ได้ติดตั้ง GitHub CLI",
|
||||
"agentManager.pr.error.gh_missing.description": "ติดตั้ง GitHub CLI เพื่อเปิดใช้งานสถานะ PR",
|
||||
}
|
||||
|
||||
+7
-1
@@ -211,7 +211,7 @@ export const dict = {
|
||||
"agentManager.review.imageUnavailable": "Bu oturum anlık görüntüsü için görsel önizlemesi kullanılamıyor.",
|
||||
"agentManager.review.endOfLongDiff": "Sonuna geldiniz!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "Çekme İsteği",
|
||||
"agentManager.import.pastePrUrl": "PR URL'sini yapıştırın...",
|
||||
"agentManager.import.open": "Aç",
|
||||
"agentManager.import.branches": "Dallar",
|
||||
@@ -222,4 +222,10 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Hiçbir dal bulunamadı.",
|
||||
"agentManager.import.noBranchesHint": "Yukarıya bir PR URL'si yapıştırın veya yeni bir worktree oluşturun.",
|
||||
"agentManager.import.failed": "İçe aktarma başarısız oldu",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "GitHub kimlik doğrulaması gerekli",
|
||||
"agentManager.pr.error.gh_auth.description":
|
||||
"PR durumunu geri yüklemek için terminalinizde 'gh auth login' komutunu çalıştırın.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI yüklü değil",
|
||||
"agentManager.pr.error.gh_missing.description": "PR durumunu etkinleştirmek için GitHub CLI'yi yükleyin.",
|
||||
}
|
||||
|
||||
+6
-1
@@ -212,7 +212,7 @@ export const dict = {
|
||||
"agentManager.review.imageUnavailable": "Попередній перегляд зображення недоступний для цього знімка сеансу.",
|
||||
"agentManager.review.endOfLongDiff": "Ви дійшли до кінця!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "Пул-реквест",
|
||||
"agentManager.import.pastePrUrl": "Вставте URL PR...",
|
||||
"agentManager.import.open": "Відкрити",
|
||||
"agentManager.import.branches": "Гілки",
|
||||
@@ -223,4 +223,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "Гілок не знайдено.",
|
||||
"agentManager.import.noBranchesHint": "Вставте URL PR вище або створіть нове робоче дерево.",
|
||||
"agentManager.import.failed": "Імпортування не вдалося",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "Потрібна автентифікація GitHub",
|
||||
"agentManager.pr.error.gh_auth.description": "Виконайте 'gh auth login' у терміналі, щоб відновити статус PR.",
|
||||
"agentManager.pr.error.gh_missing.title": "GitHub CLI не встановлено",
|
||||
"agentManager.pr.error.gh_missing.description": "Встановіть GitHub CLI, щоб увімкнути статус PR.",
|
||||
}
|
||||
|
||||
+9
-4
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "新建 Worktree",
|
||||
"agentManager.worktree.setupScript": "Worktree 设置脚本",
|
||||
"agentManager.worktree.delete": "删除 Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "删除?",
|
||||
"agentManager.worktree.stale": "已失效",
|
||||
"agentManager.worktree.staleTooltip": "在磁盘上缺失,或不再由 git worktree 跟踪",
|
||||
"agentManager.worktree.removeStale": "移除失效 Worktree",
|
||||
@@ -102,8 +102,8 @@ export const dict = {
|
||||
"agentManager.run.options": "运行选项",
|
||||
"agentManager.run.configure": "配置运行脚本",
|
||||
"agentManager.shortcuts.openAgentManager": "打开 Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "下一个代理模式",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "上一个代理模式",
|
||||
"agentManager.shortcuts.showShortcuts": "显示键盘快捷键",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "移除失效 Worktree",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "移除失效 Worktree ",
|
||||
@@ -196,7 +196,7 @@ export const dict = {
|
||||
"agentManager.review.imageUnavailable": "此会话快照无法提供图片预览。",
|
||||
"agentManager.review.endOfLongDiff": "你已经到末尾了!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "拉取请求",
|
||||
"agentManager.import.pastePrUrl": "粘贴 PR URL...",
|
||||
"agentManager.import.open": "打开",
|
||||
"agentManager.import.branches": "分支",
|
||||
@@ -207,4 +207,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "未找到分支。",
|
||||
"agentManager.import.noBranchesHint": "在上方粘贴 PR URL 或创建新的 Worktree。",
|
||||
"agentManager.import.failed": "导入失败",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "需要 GitHub 身份验证",
|
||||
"agentManager.pr.error.gh_auth.description": "在终端中运行 'gh auth login' 以恢复 PR 状态。",
|
||||
"agentManager.pr.error.gh_missing.title": "未安装 GitHub CLI",
|
||||
"agentManager.pr.error.gh_missing.description": "安装 GitHub CLI 以启用 PR 状态。",
|
||||
}
|
||||
|
||||
+9
-4
@@ -14,7 +14,7 @@ export const dict = {
|
||||
"agentManager.worktree.new": "新建 Worktree",
|
||||
"agentManager.worktree.setupScript": "Worktree 設定指令碼",
|
||||
"agentManager.worktree.delete": "刪除 Worktree",
|
||||
"agentManager.worktree.confirmDelete": "Delete?",
|
||||
"agentManager.worktree.confirmDelete": "刪除?",
|
||||
"agentManager.worktree.stale": "已失效",
|
||||
"agentManager.worktree.staleTooltip": "磁碟上不存在,或不再由 git worktree 追蹤",
|
||||
"agentManager.worktree.removeStale": "移除失效 Worktree",
|
||||
@@ -102,8 +102,8 @@ export const dict = {
|
||||
"agentManager.run.options": "執行選項",
|
||||
"agentManager.run.configure": "設定執行指令碼",
|
||||
"agentManager.shortcuts.openAgentManager": "開啟 Agent Manager",
|
||||
"agentManager.shortcuts.cycleAgentMode": "Next agent mode",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode",
|
||||
"agentManager.shortcuts.cycleAgentMode": "下一個代理模式",
|
||||
"agentManager.shortcuts.cyclePreviousAgentMode": "上一個代理模式",
|
||||
"agentManager.shortcuts.showShortcuts": "顯示鍵盤快捷鍵",
|
||||
"agentManager.dialog.removeStaleWorktree.title": "移除失效 Worktree",
|
||||
"agentManager.dialog.removeStaleWorktree.messagePre": "移除失效 Worktree ",
|
||||
@@ -196,7 +196,7 @@ export const dict = {
|
||||
"agentManager.review.imageUnavailable": "此工作階段快照無法預覽圖片。",
|
||||
"agentManager.review.endOfLongDiff": "你已經到最後了!",
|
||||
|
||||
"agentManager.import.pullRequest": "Pull Request",
|
||||
"agentManager.import.pullRequest": "提取請求",
|
||||
"agentManager.import.pastePrUrl": "貼上 PR URL...",
|
||||
"agentManager.import.open": "開啟",
|
||||
"agentManager.import.branches": "分支",
|
||||
@@ -207,4 +207,9 @@ export const dict = {
|
||||
"agentManager.import.noBranchesFound": "找不到分支。",
|
||||
"agentManager.import.noBranchesHint": "在上方貼上 PR URL 或建立新的 Worktree。",
|
||||
"agentManager.import.failed": "匯入失敗",
|
||||
|
||||
"agentManager.pr.error.gh_auth.title": "需要 GitHub 驗證",
|
||||
"agentManager.pr.error.gh_auth.description": "在終端機中執行 'gh auth login' 以還原 PR 狀態。",
|
||||
"agentManager.pr.error.gh_missing.title": "未安裝 GitHub CLI",
|
||||
"agentManager.pr.error.gh_missing.description": "安裝 GitHub CLI 以啟用 PR 狀態。",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import { createSignal } from "solid-js"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
|
||||
export function CopyButton(props: { text: string; label?: string; class?: string }) {
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(props.text)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
}
|
||||
return (
|
||||
<IconButton
|
||||
icon={copied() ? "check" : "copy"}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label={props.label ?? "Copy"}
|
||||
class={props.class}
|
||||
onClick={copy}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import { Show } from "solid-js"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import type { PRStatus } from "../../src/types/messages"
|
||||
import { prBadgeIndicator, prChecksRunning } from "../WorktreeItem"
|
||||
|
||||
const INDICATOR_ICON: Record<string, string> = {
|
||||
failure: "circle-x-outline",
|
||||
changes: "warning",
|
||||
approved: "circle-check",
|
||||
}
|
||||
|
||||
export function PRBadge(props: { pr: PRStatus }) {
|
||||
const indicator = () => prBadgeIndicator(props.pr)
|
||||
return (
|
||||
<span
|
||||
class="am-pr-panel-badge am-pr-row"
|
||||
classList={{
|
||||
"am-pr-accent-draft": props.pr.state === "draft",
|
||||
"am-pr-accent-merged": props.pr.state === "merged",
|
||||
"am-pr-accent-closed": props.pr.state === "closed",
|
||||
"am-pr-accent-pending": props.pr.state === "open" && props.pr.checks.status === "pending",
|
||||
"am-pr-accent-open": props.pr.state === "open" && props.pr.checks.status !== "pending",
|
||||
"am-pr-badge-pending": prChecksRunning(props.pr),
|
||||
}}
|
||||
>
|
||||
<Icon name="pull-request" size="small" />
|
||||
<Show when={indicator() !== "none"}>
|
||||
<Icon name={INDICATOR_ICON[indicator()]} size="small" class="am-pr-badge-status" data-status={indicator()} />
|
||||
</Show>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import { For, Show, createSignal } from "solid-js"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import type { PRStatus } from "../../src/types/messages"
|
||||
import type { PRCheck, CheckStatus } from "./pr-types"
|
||||
import { SectionHeading } from "./SectionHeading"
|
||||
import { useVSCode } from "../../src/context/vscode"
|
||||
|
||||
const CHECK: Record<CheckStatus, { icon: string; label: string }> = {
|
||||
success: { icon: "circle-check", label: "Passed" },
|
||||
failure: { icon: "circle-x-outline", label: "Failed" },
|
||||
cancelled: { icon: "circle-x-outline", label: "Cancelled" },
|
||||
skipped: { icon: "circle-x-outline", label: "Skipped" },
|
||||
pending: { icon: "play", label: "Running" },
|
||||
}
|
||||
|
||||
export function PRChecks(props: { checks: PRStatus["checks"] }) {
|
||||
const vscode = useVSCode()
|
||||
const [open, setOpen] = createSignal(true)
|
||||
return (
|
||||
<>
|
||||
<div class="am-pr-panel-divider" />
|
||||
<div class="am-pr-panel-section">
|
||||
<SectionHeading
|
||||
title="Checks"
|
||||
open={open()}
|
||||
onToggle={() => setOpen((v) => !v)}
|
||||
count={`${props.checks.passed}/${props.checks.total} passed`}
|
||||
countClass={`am-pr-checks-count-${props.checks.status}`}
|
||||
/>
|
||||
<Show when={open()}>
|
||||
<div class="am-pr-panel-checks am-pr-col">
|
||||
<For each={props.checks.checks}>
|
||||
{(check: PRCheck) => (
|
||||
<div class="am-pr-panel-check-item am-pr-row" data-status={check.status}>
|
||||
<Icon name={CHECK[check.status].icon} size="small" class="am-pr-check-icon" />
|
||||
<span class="am-pr-check-name">{check.name}</span>
|
||||
<span class="am-pr-check-status">{CHECK[check.status].label}</span>
|
||||
<Show when={check.duration}>
|
||||
<span class="am-pr-check-duration">{check.duration}</span>
|
||||
</Show>
|
||||
<Show when={check.url}>
|
||||
<Tooltip value="Open in browser" placement="bottom">
|
||||
<button
|
||||
class="am-pr-check-link"
|
||||
aria-label="Open check in browser"
|
||||
onClick={() => vscode.postMessage({ type: "openExternal", url: check.url! })}
|
||||
>
|
||||
<Icon name="link" size="small" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import { For, Show, createSignal } from "solid-js"
|
||||
import { Markdown } from "@kilocode/kilo-ui/markdown"
|
||||
import type { PRStatus } from "../../src/types/messages"
|
||||
import type { PRComment } from "./pr-types"
|
||||
import { SectionHeading } from "./SectionHeading"
|
||||
import { CopyButton } from "./CopyButton"
|
||||
|
||||
export function PRComments(props: { comments: NonNullable<PRStatus["comments"]> }) {
|
||||
const [open, setOpen] = createSignal(true)
|
||||
return (
|
||||
<>
|
||||
<div class="am-pr-panel-divider" />
|
||||
<div class="am-pr-panel-section">
|
||||
<SectionHeading
|
||||
title="Comments"
|
||||
open={open()}
|
||||
onToggle={() => setOpen((v) => !v)}
|
||||
count={props.comments.unresolved > 0 ? `${props.comments.unresolved} unresolved` : undefined}
|
||||
countClass="am-pr-panel-unresolved"
|
||||
/>
|
||||
<Show when={open()}>
|
||||
<div class="am-pr-panel-comment-list am-pr-col">
|
||||
<For each={props.comments.comments}>
|
||||
{(comment: PRComment) => (
|
||||
<div class="am-pr-panel-comment" classList={{ "am-pr-panel-comment-resolved": comment.resolved }}>
|
||||
<div class="am-pr-panel-comment-header am-pr-row">
|
||||
<span class="am-pr-panel-comment-author">{comment.author}</span>
|
||||
<Show when={comment.file}>
|
||||
<span class="am-pr-panel-comment-file">
|
||||
{comment.file}
|
||||
<Show when={comment.line}>{`:${comment.line}`}</Show>
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={comment.resolved}>
|
||||
<span class="am-pr-panel-comment-resolved-badge">Resolved</span>
|
||||
</Show>
|
||||
<CopyButton text={comment.body} class="am-pr-copy-btn" />
|
||||
</div>
|
||||
<div class="am-pr-panel-comment-body">
|
||||
<Markdown text={comment.body} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import { Show, createSignal } from "solid-js"
|
||||
import { Markdown } from "@kilocode/kilo-ui/markdown"
|
||||
import { SectionHeading } from "./SectionHeading"
|
||||
|
||||
export function PRDescription(props: { body: string }) {
|
||||
const [open, setOpen] = createSignal(true)
|
||||
return (
|
||||
<>
|
||||
<div class="am-pr-panel-divider" />
|
||||
<div class="am-pr-panel-section">
|
||||
<SectionHeading title="Description" open={open()} onToggle={() => setOpen((v) => !v)} />
|
||||
<Show when={open()}>
|
||||
<div class="am-pr-panel-description">
|
||||
<Markdown text={props.body} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import { Show } from "solid-js"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import type { WorktreeState } from "../../src/types/messages"
|
||||
import type { PRStatus } from "../../src/types/messages"
|
||||
|
||||
const STATE_LABEL: Record<PRStatus["state"], string> = {
|
||||
open: "Open",
|
||||
draft: "Draft",
|
||||
merged: "Merged",
|
||||
closed: "Closed",
|
||||
}
|
||||
|
||||
const REVIEW_LABEL: Partial<Record<NonNullable<PRStatus["review"]>, string>> = {
|
||||
approved: "Approved",
|
||||
changes_requested: "Changes Requested",
|
||||
pending: "Review Pending",
|
||||
}
|
||||
|
||||
export function PROverview(props: { pr: PRStatus; worktree?: WorktreeState }) {
|
||||
return (
|
||||
<div class="am-pr-panel-section">
|
||||
<Show when={props.worktree}>
|
||||
{(wt) => (
|
||||
<div class="am-pr-panel-row am-pr-row">
|
||||
<span class="am-pr-panel-label">Branch</span>
|
||||
<span class="am-pr-panel-value am-pr-panel-branch am-pr-row">
|
||||
<span class="am-pr-branch-name">{wt().branch}</span>
|
||||
<Icon name="arrow-right" size="small" class="am-pr-branch-arrow" />
|
||||
<span class="am-pr-branch-name">{wt().parentBranch}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<div class="am-pr-panel-row am-pr-row">
|
||||
<span class="am-pr-panel-label">Status</span>
|
||||
<span class="am-pr-panel-value" data-pr-state={props.pr.state}>
|
||||
{STATE_LABEL[props.pr.state]}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={props.pr.review}>
|
||||
{(review) => (
|
||||
<div class="am-pr-panel-row am-pr-row">
|
||||
<span class="am-pr-panel-label">Review</span>
|
||||
<span class="am-pr-panel-value">{REVIEW_LABEL[review()]}</span>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import { Component, Show } from "solid-js"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import type { WorktreeState } from "../../src/types/messages"
|
||||
import type { PRStatus } from "../../src/types/messages"
|
||||
import { PRBadge } from "./PRBadge"
|
||||
import { PROverview } from "./PROverview"
|
||||
import { PRReviewers } from "./PRReviewers"
|
||||
import { PRDescription } from "./PRDescription"
|
||||
import { PRChecks } from "./PRChecks"
|
||||
import { PRComments } from "./PRComments"
|
||||
import { PRSummary } from "./PRSummary"
|
||||
import "./pr-panel.css"
|
||||
|
||||
interface PRPanelProps {
|
||||
pr: PRStatus
|
||||
worktree?: WorktreeState
|
||||
onClose: () => void
|
||||
onOpenExternal: () => void
|
||||
}
|
||||
|
||||
export const PRPanel: Component<PRPanelProps> = (props) => {
|
||||
return (
|
||||
<div class="am-pr-panel am-pr-col">
|
||||
<div class="am-pr-panel-header am-pr-row">
|
||||
<div class="am-pr-panel-title-row am-pr-row">
|
||||
<PRBadge pr={props.pr} />
|
||||
<span class="am-pr-panel-title">{props.pr.title}</span>
|
||||
<span class="am-pr-panel-number">#{props.pr.number}</span>
|
||||
</div>
|
||||
<div class="am-pr-panel-actions am-pr-row">
|
||||
<Tooltip value="Open in browser" placement="bottom">
|
||||
<IconButton
|
||||
icon="link"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
label="Open in browser"
|
||||
onClick={props.onOpenExternal}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip value="Close" placement="bottom">
|
||||
<IconButton icon="close" size="small" variant="ghost" label="Close PR panel" onClick={props.onClose} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="am-pr-panel-body">
|
||||
<PRSummary pr={props.pr} />
|
||||
<PROverview pr={props.pr} worktree={props.worktree} />
|
||||
<Show when={(props.pr.reviewers ?? []).length > 0}>
|
||||
<PRReviewers reviewers={props.pr.reviewers ?? []} />
|
||||
</Show>
|
||||
<Show when={props.pr.body}>{(body) => <PRDescription body={body()} />}</Show>
|
||||
<Show when={props.pr.checks.total > 0}>
|
||||
<PRChecks checks={props.pr.checks} />
|
||||
</Show>
|
||||
<Show when={props.pr.comments?.total ? props.pr.comments : undefined}>
|
||||
{(comments) => <PRComments comments={comments()} />}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import { For, Show, createSignal } from "solid-js"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import type { PRReviewer, ReviewerState } from "./pr-types"
|
||||
import { SectionHeading } from "./SectionHeading"
|
||||
|
||||
const REVIEWER_ICON: Record<ReviewerState, string> = {
|
||||
approved: "circle-check",
|
||||
changes_requested: "refresh",
|
||||
commented: "edit",
|
||||
pending: "dash",
|
||||
}
|
||||
|
||||
const REVIEWER_LABEL: Record<ReviewerState, string> = {
|
||||
approved: "Approved",
|
||||
changes_requested: "Changes requested",
|
||||
commented: "Commented",
|
||||
pending: "Awaiting",
|
||||
}
|
||||
|
||||
export function PRReviewers(props: { reviewers: PRReviewer[] }) {
|
||||
const [open, setOpen] = createSignal(true)
|
||||
return (
|
||||
<>
|
||||
<div class="am-pr-panel-divider" />
|
||||
<div class="am-pr-panel-section">
|
||||
<SectionHeading title="Reviewers" open={open()} onToggle={() => setOpen((v) => !v)} />
|
||||
<Show when={open()}>
|
||||
<div class="am-pr-panel-reviewers am-pr-col">
|
||||
<For each={props.reviewers}>
|
||||
{(reviewer) => (
|
||||
<div class="am-pr-panel-reviewer am-pr-row" data-state={reviewer.state}>
|
||||
<Icon name={REVIEWER_ICON[reviewer.state]} size="small" class="am-pr-reviewer-icon" />
|
||||
<span class="am-pr-reviewer-login">{reviewer.login}</span>
|
||||
<span class="am-pr-reviewer-state">{REVIEWER_LABEL[reviewer.state]}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import { Show } from "solid-js"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import type { PRStatus } from "../../src/types/messages"
|
||||
|
||||
interface PRSummaryProps {
|
||||
pr: PRStatus
|
||||
}
|
||||
|
||||
function summaryRows(pr: PRStatus): Array<{ icon: string; label: string; status: string }> {
|
||||
const rows = []
|
||||
|
||||
if (pr.checks.total > 0) {
|
||||
const { passed, total, status } = pr.checks
|
||||
rows.push({
|
||||
icon: status === "success" ? "circle-check" : status === "failure" ? "circle-x-outline" : "play",
|
||||
label: status === "success" ? "All checks passing" : `${passed}/${total} checks passed`,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
if (pr.review) {
|
||||
const status = pr.review === "approved" ? "success" : pr.review === "changes_requested" ? "failure" : "pending"
|
||||
rows.push({
|
||||
icon: status === "success" ? "circle-check" : status === "failure" ? "circle-x-outline" : "play",
|
||||
label: status === "success" ? "Approved" : status === "failure" ? "Changes requested" : "Review pending",
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
if (pr.comments && pr.comments.unresolved > 0) {
|
||||
rows.push({
|
||||
icon: "comment",
|
||||
label: `${pr.comments.unresolved} unresolved comment${pr.comments.unresolved > 1 ? "s" : ""}`,
|
||||
status: "warning",
|
||||
})
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
export function PRSummary(props: PRSummaryProps) {
|
||||
const rows = () => summaryRows(props.pr)
|
||||
return (
|
||||
<Show when={rows().length > 0}>
|
||||
<div class="am-pr-summary">
|
||||
<div class="am-pr-summary-header am-pr-row">
|
||||
<span class="am-pr-summary-title">PR Summary</span>
|
||||
<span class="am-pr-panel-section-count am-pr-panel-diff am-pr-row">
|
||||
<Show when={props.pr.files > 0}>
|
||||
<span class="am-stat-files">{props.pr.files}f</span>
|
||||
</Show>
|
||||
<Show when={props.pr.additions > 0}>
|
||||
<span class="am-stat-additions">+{props.pr.additions}</span>
|
||||
</Show>
|
||||
<Show when={props.pr.deletions > 0}>
|
||||
<span class="am-stat-deletions">−{props.pr.deletions}</span>
|
||||
</Show>
|
||||
</span>
|
||||
</div>
|
||||
<div class="am-pr-summary-rows am-pr-col">
|
||||
{rows().map((row) => (
|
||||
<div class="am-pr-summary-row am-pr-row" data-status={row.status}>
|
||||
<Icon name={row.icon} size="small" class="am-pr-summary-icon" />
|
||||
<span class="am-pr-summary-label">{row.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @jsxImportSource solid-js */
|
||||
import { Show } from "solid-js"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
|
||||
export function SectionHeading(props: {
|
||||
title: string
|
||||
open: boolean
|
||||
onToggle: () => void
|
||||
count?: string
|
||||
countClass?: string
|
||||
}) {
|
||||
return (
|
||||
<button class="am-pr-panel-section-heading am-pr-panel-section-toggle am-pr-row" onClick={props.onToggle}>
|
||||
<span class="am-pr-panel-section-heading-left am-pr-row">
|
||||
<Icon name={props.open ? "chevron-down" : "chevron-right"} size="small" class="am-pr-section-chevron" />
|
||||
{props.title}
|
||||
</span>
|
||||
<Show when={props.count}>
|
||||
<span class={`am-pr-panel-section-count ${props.countClass ?? ""}`}>{props.count}</span>
|
||||
</Show>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
/* PR Panel */
|
||||
.am-pr-panel {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.am-pr-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.am-pr-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.am-pr-panel-header {
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 12px 8px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-title-row {
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: var(--kilo-font-size-10);
|
||||
font-variant-numeric: tabular-nums;
|
||||
background: color-mix(in srgb, var(--am-pr-accent) 15%, transparent);
|
||||
color: var(--am-pr-accent);
|
||||
border: 1px solid color-mix(in srgb, var(--am-pr-accent) 40%, transparent);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-badge.am-pr-badge-pending {
|
||||
animation: am-pr-badge-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.am-pr-panel-title {
|
||||
font-size: var(--kilo-font-size-14);
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.am-pr-panel-number {
|
||||
font-size: var(--kilo-font-size-12);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-variant-numeric: tabular-nums;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-actions {
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-section {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.am-pr-panel-section-heading {
|
||||
justify-content: space-between;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-weak);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.am-pr-panel-section-toggle {
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.am-pr-panel-section-toggle:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.am-pr-panel-section-heading-left {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.am-pr-section-chevron {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Branch → base row */
|
||||
.am-pr-panel-branch {
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.am-pr-branch-name {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: var(--kilo-font-size-11);
|
||||
}
|
||||
|
||||
.am-pr-branch-arrow {
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Check count color by status */
|
||||
.am-pr-checks-count-success {
|
||||
color: var(--vscode-testing-iconPassed, #34d399);
|
||||
}
|
||||
.am-pr-checks-count-failure {
|
||||
color: var(--vscode-testing-iconFailed, #f87171);
|
||||
}
|
||||
.am-pr-checks-count-pending {
|
||||
color: var(--vscode-charts-yellow, #fbbf24);
|
||||
}
|
||||
|
||||
.am-pr-panel-section-count {
|
||||
font-size: var(--kilo-font-size-11);
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
.am-pr-panel-divider {
|
||||
height: 1px;
|
||||
background: var(--vscode-panel-border);
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-row {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 3px 0;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.am-pr-panel-label {
|
||||
font-size: var(--kilo-font-size-11);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-weaker);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-value {
|
||||
font-size: var(--kilo-font-size-12);
|
||||
color: var(--vscode-foreground);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.am-pr-panel-value[data-pr-state="open"] {
|
||||
color: var(--vscode-testing-iconPassed, #34d399);
|
||||
}
|
||||
.am-pr-panel-value[data-pr-state="merged"] {
|
||||
color: var(--vscode-charts-purple, #a78bfa);
|
||||
}
|
||||
.am-pr-panel-value[data-pr-state="closed"] {
|
||||
color: var(--vscode-testing-iconFailed, #f87171);
|
||||
}
|
||||
.am-pr-panel-value[data-pr-state="draft"] {
|
||||
color: var(--text-weaker);
|
||||
}
|
||||
|
||||
.am-pr-panel-value[data-checks="failure"] {
|
||||
color: var(--vscode-testing-iconFailed, #f87171);
|
||||
}
|
||||
.am-pr-panel-value[data-checks="pending"] {
|
||||
color: var(--vscode-charts-yellow, #fbbf24);
|
||||
}
|
||||
.am-pr-panel-value[data-checks="success"] {
|
||||
color: var(--vscode-testing-iconPassed, #34d399);
|
||||
}
|
||||
|
||||
.am-pr-panel-diff {
|
||||
gap: 6px;
|
||||
}
|
||||
.am-pr-panel-unresolved {
|
||||
color: var(--vscode-charts-yellow, #fbbf24);
|
||||
}
|
||||
|
||||
/* PR comment list */
|
||||
.am-pr-panel-comment-list {
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.am-pr-panel-comment {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.am-pr-panel-comment-resolved {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.am-pr-panel-comment-header {
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.am-pr-panel-comment-author {
|
||||
font-size: var(--kilo-font-size-12);
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.am-pr-panel-comment-file {
|
||||
font-size: var(--kilo-font-size-11);
|
||||
color: var(--text-weak);
|
||||
font-family: var(--font-mono, monospace);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.am-pr-panel-comment-resolved-badge {
|
||||
font-size: var(--kilo-font-size-10);
|
||||
color: var(--text-weaker);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.am-pr-copy-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.am-pr-panel-comment-body [data-component="markdown"] {
|
||||
font-size: var(--kilo-font-size-12);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.am-pr-panel-description {
|
||||
line-height: 1.5;
|
||||
padding: 2px 0 4px;
|
||||
}
|
||||
|
||||
.am-pr-panel-description [data-component="markdown"],
|
||||
.am-pr-panel-comment-body [data-component="markdown"] {
|
||||
font-size: var(--kilo-font-size-13);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.am-pr-panel-description [data-component="markdown"] h1,
|
||||
.am-pr-panel-description [data-component="markdown"] h2,
|
||||
.am-pr-panel-description [data-component="markdown"] h3,
|
||||
.am-pr-panel-description [data-component="markdown"] h4,
|
||||
.am-pr-panel-description [data-component="markdown"] h5,
|
||||
.am-pr-panel-description [data-component="markdown"] h6 {
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.am-pr-panel-description [data-component="markdown"] h1 {
|
||||
font-size: var(--kilo-font-size-16);
|
||||
}
|
||||
.am-pr-panel-description [data-component="markdown"] h2 {
|
||||
font-size: var(--kilo-font-size-14);
|
||||
}
|
||||
.am-pr-panel-description [data-component="markdown"] h3 {
|
||||
font-size: var(--kilo-font-size-13);
|
||||
}
|
||||
|
||||
/* PR Reviewers */
|
||||
.am-pr-panel-reviewers {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.am-pr-panel-reviewer {
|
||||
gap: 6px;
|
||||
padding: 3px 0;
|
||||
font-size: var(--kilo-font-size-12);
|
||||
}
|
||||
|
||||
.am-pr-reviewer-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-reviewer[data-state="approved"] .am-pr-reviewer-icon {
|
||||
color: var(--vscode-testing-iconPassed, #34d399);
|
||||
}
|
||||
.am-pr-panel-reviewer[data-state="changes_requested"] .am-pr-reviewer-icon {
|
||||
color: var(--vscode-charts-yellow, #fbbf24);
|
||||
}
|
||||
.am-pr-panel-reviewer[data-state="commented"] .am-pr-reviewer-icon {
|
||||
color: var(--text-weak);
|
||||
}
|
||||
.am-pr-panel-reviewer[data-state="pending"] .am-pr-reviewer-icon {
|
||||
color: var(--text-weaker);
|
||||
}
|
||||
|
||||
.am-pr-reviewer-login {
|
||||
flex: 1;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.am-pr-reviewer-state {
|
||||
color: var(--text-weak);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-checks {
|
||||
gap: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.am-pr-panel-check-item {
|
||||
gap: 6px;
|
||||
padding: 3px 0;
|
||||
font-size: var(--kilo-font-size-12);
|
||||
}
|
||||
|
||||
.am-pr-check-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-panel-check-item[data-status="success"] .am-pr-check-icon {
|
||||
color: var(--vscode-testing-iconPassed, #34d399);
|
||||
}
|
||||
.am-pr-panel-check-item[data-status="failure"] .am-pr-check-icon {
|
||||
color: var(--vscode-testing-iconFailed, #f87171);
|
||||
}
|
||||
.am-pr-panel-check-item[data-status="cancelled"] .am-pr-check-icon {
|
||||
color: var(--text-weaker);
|
||||
}
|
||||
.am-pr-panel-check-item[data-status="skipped"] .am-pr-check-icon {
|
||||
color: var(--text-weaker);
|
||||
}
|
||||
.am-pr-panel-check-item[data-status="pending"] .am-pr-check-icon {
|
||||
color: var(--vscode-charts-yellow, #fbbf24);
|
||||
}
|
||||
|
||||
.am-pr-check-name {
|
||||
flex: 1;
|
||||
color: var(--vscode-foreground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.am-pr-check-status {
|
||||
color: var(--text-weak);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-check-duration {
|
||||
color: var(--text-weaker);
|
||||
font-size: var(--kilo-font-size-10);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-check-link {
|
||||
color: var(--text-weak);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.am-pr-check-link:hover {
|
||||
color: var(--text-base);
|
||||
}
|
||||
|
||||
/* PR toggle button in tab bar */
|
||||
.am-pr-toggle-number {
|
||||
font-size: var(--kilo-font-size-10);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* PR Summary banner */
|
||||
.am-pr-summary {
|
||||
margin: 8px 12px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.am-pr-summary-header {
|
||||
justify-content: space-between;
|
||||
padding: 6px 6px 6px 10px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.am-pr-summary-title {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-weak);
|
||||
}
|
||||
|
||||
.am-pr-summary-rows {
|
||||
padding: 6px 10px;
|
||||
gap: 4px;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.am-pr-summary-row {
|
||||
gap: 7px;
|
||||
padding: 2px 0;
|
||||
font-size: var(--kilo-font-size-12);
|
||||
}
|
||||
|
||||
.am-pr-summary-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.am-pr-summary-row[data-status="success"] .am-pr-summary-icon {
|
||||
color: var(--vscode-testing-iconPassed, #34d399);
|
||||
}
|
||||
.am-pr-summary-row[data-status="failure"] .am-pr-summary-icon {
|
||||
color: var(--vscode-testing-iconFailed, #f87171);
|
||||
}
|
||||
.am-pr-summary-row[data-status="pending"] .am-pr-summary-icon {
|
||||
color: var(--vscode-charts-yellow, #fbbf24);
|
||||
}
|
||||
.am-pr-summary-row[data-status="warning"] .am-pr-summary-icon {
|
||||
color: var(--vscode-charts-yellow, #fbbf24);
|
||||
}
|
||||
|
||||
.am-pr-summary-label {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// PR sub-types — source of truth for all PR-related types used in the PR panel.
|
||||
// PRStatus lives in src/types/messages/agent-manager.ts for broad consumption.
|
||||
|
||||
export type PRState = "open" | "draft" | "merged" | "closed"
|
||||
export type ReviewDecision = "approved" | "changes_requested" | "pending"
|
||||
export type CheckStatus = "success" | "failure" | "pending" | "skipped" | "cancelled"
|
||||
export type AggregateCheckStatus = "success" | "failure" | "pending" | "none"
|
||||
|
||||
export interface PRCheck {
|
||||
name: string
|
||||
status: CheckStatus
|
||||
url?: string
|
||||
duration?: string
|
||||
}
|
||||
|
||||
export interface PRComment {
|
||||
id: string
|
||||
author: string
|
||||
avatar?: string
|
||||
body: string
|
||||
file?: string
|
||||
line?: number
|
||||
url?: string
|
||||
resolved: boolean
|
||||
createdAt?: number
|
||||
}
|
||||
|
||||
export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented"
|
||||
|
||||
export interface PRReviewer {
|
||||
login: string
|
||||
avatar?: string
|
||||
state: ReviewerState
|
||||
}
|
||||
@@ -2,6 +2,12 @@ export const MIN_PANEL_WIDTH = 360
|
||||
const DEFAULT_PANEL_WIDTH_RATIO = 0.5
|
||||
const MAX_PANEL_WIDTH_RATIO = 0.8
|
||||
|
||||
export enum SidePanel {
|
||||
Diff = "diff",
|
||||
PR = "pr",
|
||||
Terminal = "terminal",
|
||||
}
|
||||
|
||||
function viewportWidth(viewport: number): number {
|
||||
return Number.isFinite(viewport) && viewport > 0 ? viewport : MIN_PANEL_WIDTH
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
import { createEffect, createSignal, type Accessor } from "solid-js"
|
||||
import type { ScriptTerminalStatus, TerminalStateControls } from "./state"
|
||||
import type { TerminalTabStateWithContext } from "./state"
|
||||
import type { SidePanelState } from "../AgentManagerApp"
|
||||
|
||||
interface AmbientSetupDeps {
|
||||
terms: TerminalStateControls
|
||||
selection: Accessor<string | null>
|
||||
sidePanel: Accessor<string | null>
|
||||
sidePanel: Accessor<SidePanelState>
|
||||
setSidePanel(panel: null): void
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user