Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
157079d
chore: 新增 .kcodeignore 与提示词增强实现规范
yycy134679 Sep 25, 2026
c75d4f6
feat: 提示词增强的设置 schema 与纯拼装判定模块
yycy134679 Sep 25, 2026
6c52511
feat: 输入框工具条接入提示词增强与设置分区
yycy134679 Sep 25, 2026
5bda06e
fix: 提示词增强取消改走 operationId,AbortSignal 过不了 RPC
yycy134679 Sep 25, 2026
412bda7
fix: 提示词增强补齐请求输出预算与档位契约
yycy134679 Sep 25, 2026
29d9443
docs: 提示词增强规范与实现对齐,并格式化本次新增文件
7836246 Sep 25, 2026
22bae9e
fix: workspace 生成改走流式累积,修复非标准网关上的增强失败
7836246 Sep 25, 2026
47c6f86
fix: setText 改为 discrete 同步提交,修复增强回填读回误判
7836246 Sep 25, 2026
9a35b6a
docs: 新增附件在 prompt 中的表达规范
7836246 Sep 25, 2026
e604c05
fix: 附件改为明话附件框法,不再伪装成历史 Read 工具结果
7836246 Sep 25, 2026
7af59a7
feat: 附件被截断时在消息上标出「已截断」
7836246 Sep 25, 2026
d454edb
fix: 按行限流的附件也判为截断,不再谎报内容完整
7836246 Sep 25, 2026
22bd925
feat: 增强闸门收窄为行内引用闸,附件与引用面板不再拦截
7836246 Sep 25, 2026
887fe9d
fix: 引导输入附件的截断标记改由 TurnSteerDrained 自带
yycy134679 Sep 25, 2026
dafe9e9
chore: 增强模块改 type-only 导入并规范 scope 变化日志
yycy134679 Sep 25, 2026
cee60f5
ci: 补构建 dynamic-workflow,修复 core 测试在 CI 上整文件加载失败
yycy134679 Sep 25, 2026
6d2b8bd
fix: 增强结果不再覆盖在途改动的草稿,还原点写入失败时保留
yycy134679 Sep 25, 2026
6bef4e3
fix: 修复引导输入附件投影的编译回归
yycy134679 Sep 25, 2026
409b472
fix: macOS window bounds helper 在只有 CLT 的机器上退化为单架构构建
yycy134679 Sep 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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,18 @@ jobs:
run: |
# 投影测试要加载 @kcode/contracts 的构建产物;根目录 typecheck 不会编译 CLI 包。
pnpm --filter @kcode/contracts build
# core 的附件测试经 hydrator 间接加载 workflow 编译器的 dist 产物,同样要显式构建。
pnpm --filter @kcode/dynamic-workflow build
pnpm exec tsx --test \
packages/ui/test/modelConnectivityFailure.test.ts \
packages/ui/test/loginApiKeyContinue.test.ts \
packages/ui/test/turnMetrics.test.ts \
apps/kcode-cli/packages/bootstrap/src/kcode-protocol-v4/turn-metrics-projection.test.ts \
packages/ui/test/reasoningLevelCatalog.test.ts
packages/ui/test/reasoningLevelCatalog.test.ts \
packages/ui/test/promptEnhance.test.ts \
apps/kcode-cli/packages/core/src/runtime/methods/workspace-generate-text-accumulate.test.ts \
apps/kcode-cli/packages/core/src/system-reminder/prompt-attachment.test.ts \
apps/kcode-cli/packages/core/src/system-reminder/prompt-attachment-parity.test.ts \
apps/kcode-cli/packages/core/src/runtime/helpers/attachments.test.ts \
apps/kcode-cli/packages/bootstrap/src/kcode-protocol-v4/turn-attachments-resolved-projection.test.ts
node --test packages/desktop/scripts/release-signing-plan.test.mjs
107 changes: 107 additions & 0 deletions .kcodeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
node_modules/
dist/
# kcode-server-cli 发行包 staging 输出,本地组装产物不入库。
dist-release/
# macOS CI 按架构隔离 electron-builder 输出目录,这些仍是构建产物。
packages/desktop/dist-*/
out/
prebuilds/
bundled-resources/
mock-cdn/
packages/desktop/bundled-resources/
packages/desktop/bundled-agents/
packages/desktop/bundled-tools/
packages/desktop/mock-cdn/
*.tsbuildinfo
.DS_Store
coverage/
.env.local
.env.*.local
.env
.VSCodeCounter
.idea/
.zed/
.cache
# CI 会跨任务保留这些下载缓存;不应进入 Git 未跟踪源码枚举。
/.electron-builder-cache/
/.electron-cache/
/.pnpm-store/
/.npm-cache/
.turbo/
z-work/
openclaw/
.run-logs/
.worktrees/
packages/stream-animate/dist-server/
packages/stream-animate/.stream-animate-data/
.codegraph
.mimosa/
# 集成分支曾混入 node-gyp 构建产物与 .playwright-mcp 临时快照;
# 入库由 scripts/ci/ci-repo-hygiene.mjs 在 CI 第一站拒绝,这里让本地 git add 也忽略它们。
.playwright-mcp/
*.o
.deps/
obj.target/
config.gypi
gyp-mac-tool
*.target.mk
binding.Makefile
# 这两条原来写成 build/Makefile、build/Release/*.node,含中间斜杠的 pattern
# 只相对 .gitignore 所在目录(仓库根)生效,匹配不到 packages/ax-macos/build/ 下的产物;
# 而 ci-repo-hygiene.mjs 的 denylist 用的是 (^|/)build/... 任意层级匹配。
# 宽严不一致时无斜杠 pattern 能拦住的产物会被 git add -A 重新扫进索引。
# 加 **/ 让忽略范围与门禁范围对齐。
**/build/Makefile
**/build/Release/*.node

# CUA 权限浮窗吸附用的 Swift 辅助二进制(由 prepare:macos-window-bounds 从
# packages/desktop/native/macos-window-bounds/main.swift 编出,源码入库、产物不入库)
packages/desktop/resources/macos-window-bounds/
# CUA Helper SEA 基座下载缓存(scripts/cua-helper-sea-base.mjs 落盘的官方 Node 22 运行时,纯下载产物)
/.tmp/
# Python 字节码(docker/plugin-eval/summarize.py 在主机上跑会生成)。
__pycache__/
*.pyc

# 本地工具运行产物,均可再生:桌面端 e2e 的截图/缓存/网络抓包,以及桌面
# 开发运行器的 Electron 下载缓存。
packages/desktop/.e2e-artifacts/
packages/desktop/.e2e-cache/
packages/desktop/.e2e-network-capture/
.kcode-runtime/

# desktop 源码只有 .ts。tsc -b tsconfig.main.json 会把跨 rootDir 的 import
#(例如 schedulerProtocol、armsRumShared)就地吐出 js/d.ts,正式产物在 out/。
packages/desktop/src/**/*.js
packages/desktop/src/**/*.js.map
packages/desktop/src/**/*.d.ts
packages/desktop/src/**/*.d.ts.map

# ===== ↑ 以上同步自 .gitignore(「从 .gitignore 同步」只重写以上部分)=====
.git/
.hg/
.svn/
bower_components/
jspm_packages/
site-packages/
venv/
htmlcov/
lcov-report/
cmakefiles/
cmake-build-*/
bazel-*/
pods/
deriveddata/
storybook-static/
playwright-report/
test-results/
allure-results/
allure-report/
cdk.out/
*.egg-info/
*.dist-info/
eggs/
pip-wheel-metadata/
wheels/
# ----- ↑ 以上为 KCode 默认排除规则(自定义规则请写在本行下方,不会被同步/恢复改动)-----
# 自定义规则写在下方(本行提示可删除)
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ const MEMORY_ONLY_EVENT_TYPES = new Set<string>([
SessionEventType.DynamicWorkflowRunProgress,
SessionEventType.TargetChanged,
SessionEventType.RewindTriggered,
// 附件截断事实的展示补丁只在 live 用:冷恢复由消息 parts 的 metadata.preview 派生同一份事实,
// 因此不参与冷合成,归为 memory-only,避免每次冷恢复刷一条 unclassified 诊断。
SessionEventType.TurnAttachmentsResolved,
]);

const TRANSCRIPT_DERIVED_EVENT_TYPES = new Set<string>([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ export interface CanonicalTurnAttachment {
mime: string;
bytes: number;
previewRef?: string;
/** 附件在上下文里是否被截断;缺省表示未知,不写成 false。 */
truncated?: boolean;
totalLines?: number;
}

export interface CanonicalModelStream {
Expand Down Expand Up @@ -393,6 +396,10 @@ function normalizeAttachment(attachment: TurnAttachmentMeta): CanonicalTurnAttac
fileName: attachment.fileName,
mime: attachment.mime,
bytes: attachment.bytes,
...(attachment.truncated === true ? { truncated: true } : {}),
...(attachment.truncated === true && attachment.totalLines !== undefined
? { totalLines: attachment.totalLines }
: {}),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import type {
ModelStreamingPayload,
ModelUsage,
PermissionDeniedPayload,
TurnAttachmentMeta,
TurnAttachmentsResolvedPayload,
PermissionRequestedPayload,
PermissionResolvedPayload,
SessionEvent,
Expand Down Expand Up @@ -1050,14 +1052,16 @@ export class ProductProjection {
const reduced =
event.type === SessionEventType.AssistantFeedbackUpdated
? this.onAssistantFeedbackUpdated(event)
: (() => {
const fact = normalizeConversationEvent(event, {
productTurnId,
openAssistantSegments: this.openAssistantSegments(),
});
this.normalizationDiagnostics.push(...fact.diagnostics);
return this.reduce(fact);
})();
: event.type === SessionEventType.TurnAttachmentsResolved
? this.onTurnAttachmentsResolved(event)
: (() => {
const fact = normalizeConversationEvent(event, {
productTurnId,
openAssistantSegments: this.openAssistantSegments(),
});
this.normalizationDiagnostics.push(...fact.diagnostics);
return this.reduce(fact);
})();
const subagentDeltas = this.shouldMaterializeSubagentProjection(reduced)
? this.materializeSubagentProjection(reduced)
: [];
Expand Down Expand Up @@ -2663,6 +2667,86 @@ export class ProductProjection {
return [{ op: "row.upserted", row: { ...row, state } }];
}

/**
* 附件截断事实的展示补丁。附件在 TurnStarted 之后才 resolve,因此该事实只能由
* turn_attachments_resolved 补发;这里按行内序号与既有附件对齐,只补截断字段,
* 不覆盖展示层已有的 ref / fileName / mime / bytes。缺省表示未知,不写成 false。
*/
private onTurnAttachmentsResolved(event: SessionEvent): ConversationDelta[] {
const payload = event.payload as TurnAttachmentsResolvedPayload;
const turnId = this.turnIdOf(event);
const row = this.snapshot.rows.window.find(
(candidate): candidate is UserInputRow =>
candidate.kind === "userInput" && candidate.turnId === turnId,
);
const attachments = row?.attachments;
if (!row || !attachments || attachments.length === 0) return [];

let changed = false;
const next = attachments.map((attachment, index) => {
const meta = payload.attachments[index];
if (!meta) return attachment;
const truncated = meta.truncated === true ? true : undefined;
const totalLines = truncated ? meta.totalLines : undefined;
if (attachment.truncated === truncated && attachment.totalLines === totalLines) {
return attachment;
}
changed = true;
return {
...attachment,
...(truncated ? { truncated: true } : {}),
...(totalLines !== undefined ? { totalLines } : {}),
};
});
if (!changed) return [];
return [{ op: "row.upserted", row: { ...row, attachments: next } }];
}

/**
* steer-drain 输入行的附件展示字段。guide 内联进当前轮、没有
* turn_attachments_resolved 补发,截断事实只能由 drainedInputs[].attachments
* 自带;基准仍取 intent.attachmentRefs(保留稳定 ref / previewRef),按行内序号
* 合并截断字段。无 refs 时直接采用自带元信息(无 ref 的附件在此处没有
* turn-attachment 占位口径可用,展示层只用 fileName/mime/bytes)。
*/
private steerDrainedRowAttachments(item: {
intent?: TurnInputIntentMetadata;
attachments?: TurnAttachmentMeta[];
// 两个分支都必有 attachments,只有「既无 refs 也无自带元信息」返回空对象(调用方 spread,
// 字段缺席即「无附件」),所以这里只能声明为可选。
}): { attachments?: NonNullable<UserInputRow["attachments"]> } {
const refs = item.intent?.attachmentRefs;
const metas = item.attachments;
if (refs?.length) {
return {
attachments: refs.map((attachment, index) => {
const meta = metas?.[index];
const truncated = meta?.truncated === true;
return {
...attachment,
...(truncated ? { truncated: true } : {}),
...(truncated && meta?.totalLines !== undefined ? { totalLines: meta.totalLines } : {}),
};
}),
};
}
if (metas?.length) {
return {
attachments: metas.map((meta) => ({
ref: meta.ref ?? "",
fileName: meta.fileName,
mime: meta.mime,
bytes: meta.bytes,
...(meta.truncated === true ? { truncated: true } : {}),
...(meta.truncated === true && meta.totalLines !== undefined
? { totalLines: meta.totalLines }
: {}),
})),
};
}
return {};
}

private onAssistantFeedbackUpdated(event: SessionEvent): ConversationDelta[] {
const payload = event.payload as AssistantFeedbackUpdatedPayload;
const row = this.snapshot.rows.window.find(
Expand Down Expand Up @@ -3611,7 +3695,7 @@ export class ProductProjection {
...(item.intent?.sourceCommandId ? { sourceCommandId: item.intent.sourceCommandId } : {}),
...(rootSourceCommandId ? { rootSourceCommandId } : {}),
...(item.intent?.clientId ? { clientId: item.intent.clientId } : {}),
...(item.intent?.attachmentRefs?.length ? { attachments: item.intent.attachmentRefs } : {}),
...this.steerDrainedRowAttachments(item),
};
// queue/guide 消费后的 real-user row 与普通 TurnStarted 共用完整 canonical target;
// 缺 messageId 的旧事件仍只可展示,不暴露无法执行的 edit action。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ export function buildUserInputRow(base: RowBaseInput, payload: TurnStartedPayloa
fileName: meta.fileName,
mime: meta.mime,
bytes: meta.bytes,
// 冷恢复(transcript 合成)时元信息由消息 parts 派生,这里能带上截断事实;
// live 的 TurnStarted 阶段还不知道,缺省即未知。
...(meta.truncated === true ? { truncated: true } : {}),
...(meta.truncated === true && meta.totalLines !== undefined
? { totalLines: meta.totalLines }
: {}),
}));
const sourceCommandId = payload.intent?.sourceCommandId ?? payload.inputId;
const rootSourceCommandId = payload.intent?.provenance?.sourceCommandId ?? sourceCommandId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,18 +531,32 @@ function subagentStatusFromToolPart(
*/
function attachmentMetasOfMessage(
parts: readonly MessagePart[],
): Array<{ fileName: string; mime: string; bytes: number; ref?: string }> {
): Array<{
fileName: string;
mime: string;
bytes: number;
ref?: string;
truncated?: boolean;
totalLines?: number;
}> {
const fileParts = parts.filter(
(part): part is Extract<MessagePart, { type: "file" }> => part.type === "file",
);
return fileParts.map((part, index) => {
const urlIsStableRef = part.url.length > 0 && !part.url.startsWith("data:");
const basenameFromUrl = urlIsStableRef ? (part.url.split(/[\\/]/).pop() ?? "") : "";
// 截断事实在 resolve 时写进 part.metadata.preview,冷恢复据此与 live 的
// turn_attachments_resolved 给出一致结果;缺省即未知。
const truncated = part.metadata?.preview?.truncated === true;
return {
fileName: part.filename ?? (basenameFromUrl || `attachment-${index + 1}`),
mime: part.mime,
bytes: part.metadata?.sizeBytes ?? 0,
...(urlIsStableRef ? { ref: part.url } : {}),
...(truncated ? { truncated: true } : {}),
...(truncated && part.metadata?.preview?.totalLines !== undefined
? { totalLines: part.metadata.preview.totalLines }
: {}),
};
});
}
Expand Down Expand Up @@ -1410,6 +1424,9 @@ function collectTurnOutput(options: {
// sourceCommandId/clientId/attachments,命令去重与展示也不再和 live 等价。
// 新数据优先复用原 queueItemId,legacy 才使用可诊断的 hydration fallback。
const pendingInputId = intent?.queueItemId ?? `hydrate-steer-${String(message.info.id)}`;
// guide 合成事件同样自带附件元信息(含截断事实,从已持久化 parts 派生),
// 与 live drain 的 drainedInputs[].attachments 同一形状与口径。
const attachmentMetas = attachmentMetasOfMessage(message.parts);
push(
SessionEventType.TurnSteerDrained,
{
Expand All @@ -1422,6 +1439,7 @@ function collectTurnOutput(options: {
text: textOfMessage(message.parts),
delivery: "guide",
...(intent ? { intent } : {}),
...(attachmentMetas.length > 0 ? { attachments: attachmentMetas } : {}),
},
],
targetTurnId: turnId,
Expand Down
Loading