Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
6874031
feat(mcp): support stdio server cwd
luoye520ww Jun 23, 2026
34d94c0
feat(sidebar): improve session actions
luoye520ww Jun 23, 2026
2619553
Merge pull request #539 from luoye520ww/codex/sidebar-session-actions
XingYu-Zhong Jun 23, 2026
356d6bc
fix(security): git-checkpoint restore busy guard reads thread.status …
whitelonng Jun 23, 2026
57d782a
fix(security): harden git-checkpoint traversal and enforce MCP HTTPS …
whitelonng Jun 23, 2026
7cbbf4d
feat(subagents): route delegated children to a per-profile provider
XingYu-Zhong Jun 23, 2026
0a01b90
feat(subagents): P1a — SubagentMode, per-profile systemPrompt + allow…
XingYu-Zhong Jun 23, 2026
4d34618
feat(subagents): P1b — SubagentsView CRUD, settings types, runtime co…
XingYu-Zhong Jun 23, 2026
d649dde
feat(subagents): P2 — thread agentId+persona, composer agent picker, …
XingYu-Zhong Jun 23, 2026
dfcaf30
feat(subagents): P3 — .kun/agents/*.md overlay, AI draft, detach + abort
XingYu-Zhong Jun 23, 2026
b0dcd16
fix: make release artifact names explicit
musnows Jun 23, 2026
e445b69
fix(subagents): augment persona prompt + refresh agent picker on open
XingYu-Zhong Jun 23, 2026
4c645f9
Merge pull request #2 from musnows/fix/daily-dev-artifact-names
musnows Jun 24, 2026
73bcbe2
Merge pull request #550 from musnows/develop
musnows Jun 24, 2026
6e5fa86
revert(release): remove explicit artifact names
musnows Jun 24, 2026
d0ac813
Merge pull request #551 from musnows/codex/revert-pr-550-artifact-names
musnows Jun 24, 2026
cf2d4df
fix(ci): relax daily prerelease artifact checks (#553)
musnows Jun 24, 2026
afbcfa6
fix(chat): keep timeline jump rail fixed (#548)
musnows Jun 24, 2026
acdf67b
fix(branch-picker): handle long branch names (#549)
musnows Jun 24, 2026
f417611
fix(settings): show full workspace path (#545)
musnows Jun 24, 2026
27fbcc5
fix(chat): update context usage while streaming (#542)
musnows Jun 24, 2026
c554f67
fix(runtime): require health-probe before declaring startup ready (#541)
XingYu-Zhong Jun 24, 2026
6bc679a
feat(onboarding): configure default agent permissions (#543)
musnows Jun 24, 2026
2f952e4
fix(security): IM permission passthrough + workspace symlink escape (…
whitelonng Jun 24, 2026
9c7c095
feat(settings): add git checkpoint cleanup interval, opt-in (#547)
musnows Jun 24, 2026
4bbccd4
feat(chat): support scoped skills and document attachments
luoye520ww Jun 24, 2026
21ea378
feat(tool-runtime): add global skill loading, source tracking, and de…
yuanchenglu Jun 23, 2026
ec2753a
fix(tool-runtime): wire global skill roots from settings to runtime c…
yuanchenglu Jun 24, 2026
06ae3fd
Merge pull request #535 from luoye520ww/codex/issue-534-mcp-cwd
XingYu-Zhong Jun 24, 2026
821aaa3
Merge pull request #560 from yuanchenglu/pr/tool-runtime
XingYu-Zhong Jun 24, 2026
8037402
Merge remote-tracking branch 'origin/develop' into codex/issues-554-5…
XingYu-Zhong Jun 24, 2026
8d0234d
Merge pull request #559 from luoye520ww/codex/issues-554-556-557-558
XingYu-Zhong Jun 24, 2026
e5e3f22
feat(chat): add process tool icons
musnows Jun 24, 2026
2bc90bc
fix(chat): show chrome for plain text code blocks
musnows Jun 24, 2026
3cbef88
fix(skills): honor disabled skills in runtime + togglable Codex plugi…
XingYu-Zhong Jun 24, 2026
a0495be
feat(subagents): configurable Compaction model + preset General/Explo…
XingYu-Zhong Jun 24, 2026
cc3922d
Merge feature/subagent-system into develop
XingYu-Zhong Jun 24, 2026
2aad359
feat(agents): rewrite agent config UI — model config + full managemen…
XingYu-Zhong Jun 24, 2026
8b38d7d
fix(kun): stop dropping/truncating tool calls in streaming model client
XingYu-Zhong Jun 24, 2026
3228f88
Merge fix/tool-call-streaming-finalization into develop
XingYu-Zhong Jun 24, 2026
800f799
feat(composer): dock ask-user prompt above the composer | 询问用户面板上移至输入框上方
XingYu-Zhong Jun 24, 2026
8b284c0
fix(model): merge parallel tool_results into one Anthropic user message
musnows Jun 25, 2026
f94e24f
feat(settings): annotate built-in endpoint formats with vendor (opena…
musnows Jun 25, 2026
e192134
fix(workflow): offset Loop editor below Windows titlebar (#570)
XingYu-Zhong Jun 25, 2026
6ca03a6
refactor(kun): rewrite context compaction summary as opencode-style c…
XingYu-Zhong Jun 25, 2026
bdf8d71
fix(chat): only lock vision→text model switch when images present (#579)
XingYu-Zhong Jun 25, 2026
8595cbb
fix(runtime): distinguish upstream model fetch_failed from local runt…
whitelonng Jun 25, 2026
0c01c78
feat: enhance subagent functionality and UI font scaling
XingYu-Zhong Jun 25, 2026
48a51ba
feat: implement blocked tools and servers handling in MCP search prov…
XingYu-Zhong Jun 25, 2026
50d1e5a
Merge pull request #584 from whitelonng/codex/fix-573-fetch-failed
XingYu-Zhong Jun 25, 2026
343e3bd
Merge pull request #575 from musnows/codex/fix-anthropic-parallel-too…
XingYu-Zhong Jun 25, 2026
0515b64
Merge pull request #576 from musnows/codex/annotate-endpoint-format-v…
XingYu-Zhong Jun 25, 2026
105bc53
Merge pull request #569 from musnows/codex/plain-text-code-block-banner
XingYu-Zhong Jun 25, 2026
81935a5
Merge pull request #568 from musnows/codex/add-process-tool-icons
XingYu-Zhong Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/daily-dev-prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,6 @@ jobs:
"Kun-${DEV_VERSION}-mac-x64.zip"
"Kun-${DEV_VERSION}-win-x64.exe"
"Kun-${DEV_VERSION}-linux-x86_64.AppImage"
"latest-mac.yml"
"latest.yml"
"latest-linux.yml"
)

for file in "${required[@]}"; do
Expand Down
4 changes: 3 additions & 1 deletion kun/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ Shape:
"enabled": true,
"transport": "stdio",
"command": "npx",
"cwd": "/path/to/workspace",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "<github-token>" },
"trustScope": "workspace",
Expand Down Expand Up @@ -453,7 +454,8 @@ stay local to one thread, leave it as a pinned constraint.
server `enabled` flag, transport-specific fields (`command` for
`stdio`, `url` for HTTP/SSE), `trustedWorkspaceRoots` for
workspace-scoped servers, and `/v1/runtime/tools` for redacted
`lastError` diagnostics.
`lastError` diagnostics. Stdio servers can set `cwd`; if omitted,
workspace-scoped servers start in the first trusted workspace root.
- Web tools are missing: `capabilities.web.enabled` must be true and
at least one of `fetchEnabled` / `searchEnabled` must be true.
Built-in fetch handles HTTP(S) pages; search may still be
Expand Down
3 changes: 2 additions & 1 deletion kun/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ Kun 使用 JSON 配置文件管理运行时行为,避免重建后重配或硬
"enabled": true,
"transport": "stdio",
"command": "npx",
"cwd": "/path/to/workspace",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "<github-token>" },
"trustScope": "workspace",
Expand Down Expand Up @@ -391,7 +392,7 @@ SSE 使用 `id: <seq>`、`event: <kind>` 与 `data:`。新连接可通过 `since

## 故障排查

- MCP 不出现:检查 `capabilities.mcp.enabled`、服务器的 `enabled` 开关、`transport` 字段(`stdio` 需检查 `command`,HTTP/SSE 需检查 `url`)、workspace 级服务器的 `trustedWorkspaceRoots`,以及 `/v1/runtime/tools` 的 `lastError`。
- MCP 不出现:检查 `capabilities.mcp.enabled`、服务器的 `enabled` 开关、`transport` 字段(`stdio` 需检查 `command`,HTTP/SSE 需检查 `url`)、workspace 级服务器的 `trustedWorkspaceRoots`,以及 `/v1/runtime/tools` 的 `lastError`。stdio 服务器可配置 `cwd`;未配置时,workspace 级服务器会在第一个受信任工作区根目录中启动。
- Web 工具不可用:检查 `capabilities.web.enabled`,并确保 `fetchEnabled` / `searchEnabled` 至少一项为 true。内置 provider 负责抓取 HTTP(S) 页面,搜索可能因未实现 provider 而不可用。
- 图片上传失败:检查 `maxImageBytes`、`maxImageDimension`、`allowedMimeTypes` 与文本 fallback 的大小限制。纯文本模型需要足够小的 base64 文本 fallback。
- 记忆未注入:确认 `capabilities.memory` 为 true,`/v1/memory/diagnostics` 显示正常,作用域与工作区匹配且未被禁用;再看 `lastInjectedIds`。
Expand Down
1 change: 1 addition & 0 deletions kun/src/adapters/in-memory-approval-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class InMemoryApprovalGate implements ApprovalGate {
decide(approvalId: string, decision: 'allow' | 'deny', reason?: string): boolean {
const approval = this.approvals.get(approvalId)
if (!approval) return false
if (approval.status !== 'pending') return false
const resolved = resolveApprovalRequest(approval, decision, reason)
this.approvals.set(approvalId, resolved)
const resolver = this.resolvers.get(approvalId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { describe, expect, it } from 'vitest'
import { CompatModelClient } from './compat-model-client.js'
import type { ModelCapabilityMetadata } from '../../contracts/capabilities.js'
import type { ModelRequest, ModelStreamChunk } from '../../ports/model-client.js'

// These tests exercise the REAL streaming SSE path (`streamSse` /
// `consumeStreamPayload`), which had no coverage before. They lock in the fix
// for the silent tool-call drop: the chat_completions branch only finalized on
// `finish_reason === 'tool_calls'`, so a provider ending with 'stop', 'length',
// or a bare `[DONE]` while a tool call was pending dropped it entirely.

type CapturedCall = { url: string; body: Record<string, unknown> }

function sseResponse(frames: string[]): Response {
const encoder = new TextEncoder()
const stream = new ReadableStream<Uint8Array>({
start(controller) {
for (const frame of frames) controller.enqueue(encoder.encode(frame))
controller.close()
}
})
return new Response(stream, {
status: 200,
headers: { 'content-type': 'text/event-stream' }
})
}

function frame(payload: unknown): string {
return `data: ${JSON.stringify(payload)}\n\n`
}

function streamingFetch(frames: string[], calls: CapturedCall[] = []): typeof fetch {
return (async (url: string, init: { body: string }) => {
calls.push({ url: String(url), body: JSON.parse(init.body) as Record<string, unknown> })
return sseResponse(frames)
}) as unknown as typeof fetch
}

function capability(overrides: Partial<ModelCapabilityMetadata> = {}): (model: string) => ModelCapabilityMetadata {
return (model) => ({
id: model,
inputModalities: ['text'],
outputModalities: ['text'],
supportsToolCalling: true,
messageParts: ['text'],
...overrides
})
}

function request(overrides: Partial<ModelRequest> = {}): ModelRequest {
return {
threadId: 't1',
turnId: 'u1',
model: 'test-model',
systemPrompt: 'You are a helpful assistant.',
prefix: [],
history: [],
tools: [{ name: 'edit', description: 'edit a file', inputSchema: { type: 'object' } }],
abortSignal: new AbortController().signal,
...overrides
}
}

async function drain(iterable: AsyncIterable<ModelStreamChunk>): Promise<ModelStreamChunk[]> {
const chunks: ModelStreamChunk[] = []
for await (const chunk of iterable) chunks.push(chunk)
return chunks
}

function toolCallCompletes(
chunks: ModelStreamChunk[]
): Extract<ModelStreamChunk, { kind: 'tool_call_complete' }>[] {
return chunks.filter(
(c): c is Extract<ModelStreamChunk, { kind: 'tool_call_complete' }> =>
c.kind === 'tool_call_complete'
)
}

function completed(chunks: ModelStreamChunk[]): Extract<ModelStreamChunk, { kind: 'completed' }> {
const last = chunks.at(-1)
if (!last || last.kind !== 'completed') throw new Error('stream did not end with completed')
return last
}

function chatToolDelta(d: { index: number; id?: string; name?: string; args?: string }): string {
const fn: Record<string, unknown> = {}
if (d.name !== undefined) fn.name = d.name
if (d.args !== undefined) fn.arguments = d.args
const call: Record<string, unknown> = { index: d.index, function: fn }
if (d.id !== undefined) call.id = d.id
return frame({ choices: [{ index: 0, delta: { tool_calls: [call] } }] })
}

function chatFinish(reason: string): string {
return frame({ choices: [{ index: 0, delta: {}, finish_reason: reason }] })
}

// A two-part chat_completions tool-call stream. The args split across deltas so
// the test also covers index-based continuation accumulation.
function chatToolCallDeltas(): string[] {
return [
chatToolDelta({ index: 0, id: 'call_1', name: 'edit', args: '{"path":' }),
chatToolDelta({ index: 0, args: '"a.txt"}' })
]
}

function makeClient(fetchImpl: typeof fetch, modelCapabilities?: (model: string) => ModelCapabilityMetadata) {
return new CompatModelClient({
baseUrl: 'https://provider.example/v1/chat/completions',
apiKey: 'sk-test',
model: 'test-model',
endpointFormat: 'chat_completions',
fetchImpl,
...(modelCapabilities ? { modelCapabilities } : {})
})
}

describe('CompatModelClient streaming tool-call finalization', () => {
it('emits a tool call when chat_completions ends with finish_reason "tool_calls" (no double emit)', async () => {
const frames = [...chatToolCallDeltas(), chatFinish('tool_calls'), 'data: [DONE]\n\n']
const chunks = await drain(makeClient(streamingFetch(frames)).stream(request()))
const calls = toolCallCompletes(chunks)
expect(calls).toHaveLength(1)
expect(calls[0].toolName).toBe('edit')
expect(calls[0].arguments).toEqual({ path: 'a.txt' })
expect(completed(chunks).stopReason).toBe('tool_calls')
})

it('recovers a tool call the provider mislabeled as finish_reason "stop"', async () => {
// Regression: previously dropped silently because finishReason !== 'tool_calls'.
const frames = [...chatToolCallDeltas(), chatFinish('stop'), 'data: [DONE]\n\n']
const chunks = await drain(makeClient(streamingFetch(frames)).stream(request()))
const calls = toolCallCompletes(chunks)
expect(calls).toHaveLength(1)
expect(calls[0].arguments).toEqual({ path: 'a.txt' })
// A recovered call means it was really a tool-call turn.
expect(completed(chunks).stopReason).toBe('tool_calls')
})

it('recovers a tool call when the stream ends with a bare [DONE] and no finish_reason', async () => {
const frames = [...chatToolCallDeltas(), 'data: [DONE]\n\n']
const chunks = await drain(makeClient(streamingFetch(frames)).stream(request()))
expect(toolCallCompletes(chunks)).toHaveLength(1)
expect(completed(chunks).stopReason).toBe('tool_calls')
})

it('surfaces truncated arguments as __raw (instead of dropping) on finish_reason "length"', async () => {
// Only the first (incomplete) delta arrives, then the model hits its cap.
const frames = [
chatToolDelta({ index: 0, id: 'call_1', name: 'edit', args: '{"path":' }),
chatFinish('length'),
'data: [DONE]\n\n'
]
const chunks = await drain(makeClient(streamingFetch(frames)).stream(request()))
const calls = toolCallCompletes(chunks)
expect(calls).toHaveLength(1)
expect(calls[0].arguments).toHaveProperty('__raw', '{"path":')
// Truncation stays visible as 'length' so the loop can warn the user.
expect(completed(chunks).stopReason).toBe('length')
})

it('does not emit a tool call when no tool deltas were streamed', async () => {
const frames = [
frame({ choices: [{ index: 0, delta: { content: 'hello' } }] }),
chatFinish('stop'),
'data: [DONE]\n\n'
]
const chunks = await drain(makeClient(streamingFetch(frames)).stream(request()))
expect(toolCallCompletes(chunks)).toHaveLength(0)
expect(completed(chunks).stopReason).toBe('stop')
})

it('recovers an Anthropic Messages tool_use block cut off before content_block_stop', async () => {
const frames = [
frame({ type: 'message_start', message: { usage: { input_tokens: 10 } } }),
frame({ type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'toolu_1', name: 'edit' } }),
frame({ type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: '{"path":"a.txt"}' } }),
// No content_block_stop — stream is cut off, then the message ends.
frame({ type: 'message_delta', delta: { stop_reason: 'max_tokens' } }),
frame({ type: 'message_stop' })
]
const client = new CompatModelClient({
baseUrl: 'https://provider.example/anthropic',
apiKey: 'sk-test',
model: 'test-model',
endpointFormat: 'messages',
fetchImpl: streamingFetch(frames)
})
const chunks = await drain(client.stream(request()))
const calls = toolCallCompletes(chunks)
expect(calls).toHaveLength(1)
expect(calls[0].toolName).toBe('edit')
expect(calls[0].arguments).toEqual({ path: 'a.txt' })
})
})

describe('CompatModelClient output-token cap', () => {
function captureMessagesBody(
cap: (model: string) => ModelCapabilityMetadata,
req: Partial<ModelRequest> = {}
): Promise<Record<string, unknown>> {
const calls: CapturedCall[] = []
const frames = [frame({ type: 'message_start', message: { usage: {} } }), frame({ type: 'message_stop' })]
const client = new CompatModelClient({
baseUrl: 'https://provider.example/anthropic',
apiKey: 'sk-test',
model: 'test-model',
endpointFormat: 'messages',
fetchImpl: streamingFetch(frames, calls),
modelCapabilities: cap
})
return drain(client.stream(request(req))).then(() => calls[0].body)
}

it('gives reasoning (anthropic-thinking) models a large messages max_tokens default', async () => {
const body = await captureMessagesBody(
capability({ reasoning: { supportedEfforts: ['auto', 'off'], defaultEffort: 'auto', requestProtocol: 'anthropic-thinking' } }),
{ reasoningEffort: 'auto' }
)
expect(body.max_tokens).toBe(32_768)
})

it('uses the smaller messages default for non-reasoning models', async () => {
const body = await captureMessagesBody(capability())
expect(body.max_tokens).toBe(8_192)
})

it('lets a per-model maxOutputTokens capability override the default', async () => {
const body = await captureMessagesBody(
capability({
maxOutputTokens: 5_000,
reasoning: { supportedEfforts: ['auto', 'off'], defaultEffort: 'auto', requestProtocol: 'anthropic-thinking' }
}),
{ reasoningEffort: 'auto' }
)
expect(body.max_tokens).toBe(5_000)
})

it('lets an explicit request.maxTokens win over everything', async () => {
const body = await captureMessagesBody(capability({ maxOutputTokens: 5_000 }), { maxTokens: 1_234 })
expect(body.max_tokens).toBe(1_234)
})
})
Loading
Loading