Merge latest master into feat/mcp-auto-reconnect
Retarget PR #2187 from master 8a763b34645fc9371c0c21595ac65438d603d9bf to c757901957abdfd87f1cd7b11ecab16f3050e65d so the reconnect changes are evaluated against the current release and package metadata. Resolve the mcp-client package.json conflict by preserving master's publishable-package metadata and workspace peer ranges while retaining the PR's direct @deepseek-ai/dsh-timeout peer and development dependency. The reconnect implementation therefore keeps its explicit runtime contract without discarding current-master release configuration.
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-acp",
|
||||
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/acp/acp"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -26,14 +33,14 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.25.1",
|
||||
"@deepseek-ai/schemastery": "^3.17.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-user-approval": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
@@ -44,6 +51,6 @@
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-gateway",
|
||||
"description": "TypeRT Remote Host dispatcher and Client API endpoint",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/api/gateway"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -52,17 +59,17 @@
|
||||
"@deepseek-ai/dsh-type-meta": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-connection": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-typert-registry": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-api-remotes",
|
||||
"description": "Remote BFF assembly and Host Agent/Session lookup policy",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/api/remotes"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -46,13 +53,13 @@
|
||||
"@deepseek-ai/dsh-type-meta": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-goal": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-persistence": "^0.0.1",
|
||||
"@deepseek-ai/dsh-typert-registry": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
@@ -61,6 +68,6 @@
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-attachment-local",
|
||||
"description": "Private content-addressed DSH_HOME attachment storage",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/attachment/attachment-local"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -15,19 +22,19 @@
|
||||
"files": ["lib/index.js", "lib/invariant.js", "lib/types/**/*.d.ts"],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-attachment": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-paths": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-paths": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "^3.18.0",
|
||||
"@deepseek-ai/schemastery": "workspace:^",
|
||||
"sharp": "^0.35.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-paths": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-attachment",
|
||||
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/attachment/attachment"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -15,13 +22,13 @@
|
||||
"files": ["lib/index.js", "lib/invariant.js", "lib/types/**/*.d.ts"],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-brand": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-bash-env",
|
||||
"description": "Tool-independent managed DSH_* shell environment registry",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bash/bash-env"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -25,15 +32,15 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-bash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-paths": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-persistence": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-paths": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
@@ -43,6 +50,6 @@
|
||||
"@deepseek-ai/dsh-paths": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-bash-local",
|
||||
"description": "Local-subprocess implementation of the DeepSeek Harness bash executor seam",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bash/bash-local"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -25,14 +32,14 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-bash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-subprocess": "^0.0.1",
|
||||
"@deepseek-ai/dsh-timeout": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
@@ -40,6 +47,6 @@
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-bash-sandbox",
|
||||
"description": "Sandbox-consuming implementation of the DeepSeek Harness bash executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bash/bash-sandbox"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -25,12 +32,12 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-bash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-bash-local": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
@@ -40,7 +47,7 @@
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/node-addon-landlock-run": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-bash",
|
||||
"description": "Abstract bash executor seam (ctx.bash) for the DeepSeek Harness",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bash/bash"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -25,15 +32,15 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-subprocess": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-pwsh-local",
|
||||
"description": "Local PowerShell implementation of the DeepSeek Harness bash executor seam",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bash/pwsh-local"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -25,14 +32,14 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-bash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-subprocess": "^0.0.1",
|
||||
"@deepseek-ai/dsh-timeout": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
@@ -40,6 +47,6 @@
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-pwsh-sandbox",
|
||||
"description": "Sandbox-consuming implementation of the DeepSeek Harness PowerShell executor seam (confines every command via ctx.sandbox, reports denial/enforcement result facts)",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bash/pwsh-sandbox"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -25,12 +32,12 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-bash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-pwsh-local": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
@@ -40,6 +47,6 @@
|
||||
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-tool-bash",
|
||||
"description": "Model-facing bash tool with optional generic background-task and sandbox-escalation support",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bash/tool-bash"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -25,21 +32,21 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-bash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-bash-env": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tasks": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"@deepseek-ai/dsh-user-approval": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash-env": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tasks": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
@@ -61,6 +68,6 @@
|
||||
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-tool-pwsh",
|
||||
"description": "Model-facing pwsh tool over the bash executor seam",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bash/tool-pwsh"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -25,21 +32,21 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-bash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-bash-env": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox": "^0.0.1",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tasks": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"@deepseek-ai/dsh-user-approval": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash-env": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tasks": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
@@ -58,6 +65,6 @@
|
||||
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-app-boot",
|
||||
"description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/boot/app-boot"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -28,15 +35,15 @@
|
||||
"js-yaml": "^4.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-group": "^1.0.0",
|
||||
"@deepseek-ai/cordis-plugin-hmr": "^1.0.15",
|
||||
"@deepseek-ai/cordis-plugin-include": "^1.0.4",
|
||||
"@deepseek-ai/cordis-plugin-loader": "^1.0.0-rc.5",
|
||||
"@deepseek-ai/dsh-environment": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-paths": "^0.0.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis-plugin-group": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-hmr": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-include": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-environment": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-paths": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@deepseek-ai/cordis-plugin-hmr": {
|
||||
@@ -54,6 +61,6 @@
|
||||
"@deepseek-ai/dsh-paths": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-cmdline",
|
||||
"description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/boot/cmdline"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -25,15 +32,15 @@
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-loader": "^1.0.0-rc.5",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-include": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"commander": "^15.0.0",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-base",
|
||||
"description": "The shared dsh core as a profile bundle: every profile's first patch layer, inserting the base plugin rows over the empty profile root",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bundle/base"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -34,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/cordis-plugin-hmr": "workspace:*",
|
||||
"@deepseek-ai/cordis-plugin-timer": "workspace:*",
|
||||
"@deepseek-ai/cordis-plugin-hmr": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-timer": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-default-model": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
@@ -115,11 +122,11 @@
|
||||
"@deepseek-ai/dsh-workspace-context": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-headless",
|
||||
"description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bundle/headless"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -39,17 +46,17 @@
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-cmdline": "workspace:^",
|
||||
"@deepseek-ai/dsh-code-runtime-worker": "workspace:^",
|
||||
"@deepseek-ai/schemastery": "^3.18.0",
|
||||
"@deepseek-ai/schemastery": "workspace:^",
|
||||
"commander": "^15.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-loader": "^1.0.0-rc.5",
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-agent-default-model": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-default-model": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
@@ -58,6 +65,6 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-web-app",
|
||||
"description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/bundle/web-app"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -80,21 +87,21 @@
|
||||
"@deepseek-ai/dsh-storage-domain": "workspace:^",
|
||||
"@deepseek-ai/dsh-storage-json": "workspace:^",
|
||||
"@deepseek-ai/dsh-workspace": "workspace:^",
|
||||
"@deepseek-ai/schemastery": "^3.18.0",
|
||||
"@deepseek-ai/schemastery": "workspace:^",
|
||||
"commander": "^15.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-loader": "^1.0.0-rc.5",
|
||||
"@deepseek-ai/dsh-bash-env": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash-env": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-bash-env": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-connection",
|
||||
"description": "Wire consumer layer: HTTP-up/WebSocket-down client, ConnectionController dual streams with reconnect, and fixture api",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/connection"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -37,7 +44,7 @@
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/schemastery": "^3.18.0",
|
||||
"@deepseek-ai/schemastery": "workspace:^",
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
"files": [
|
||||
@@ -47,14 +54,14 @@
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-host-webserver": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-hmr",
|
||||
"description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/hmr"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -31,21 +38,21 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-loader": "^1.0.0-rc.5",
|
||||
"@deepseek-ai/dsh-client-modules": "^0.0.1",
|
||||
"@deepseek-ai/dsh-host-webserver": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-modules": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-modules": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-locale",
|
||||
"description": "Locale plugin: Host-backed zh/en preference, browser-derived fallback, locale snapshots, and typed namespace dictionaries",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/locale"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -34,12 +41,12 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-connection": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,12 +55,12 @@
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-modules",
|
||||
"description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/modules"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -38,7 +45,7 @@
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
@@ -47,7 +54,7 @@
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/runtime/README.md
|
||||
README.md: 1ec6cc38aed1bebff6b6ecb40faee7ae3ba9e412
|
||||
README.zh.md: 6602152790a1d433371e27b274a4eb8c9e3cfcd8
|
||||
README.md: d84cd793c34242759ad04edf0debb91558ec3dfc
|
||||
README.zh.md: e7a74c454f24fcec5e797427c21222b1dc258b44
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Client cordis boot and React-free object services: SlotsService wraps SlotCore and supplies renderer data sources; SessionsService owns Session objects and the Chat-facing list, scope, and event-window state; SessionHistoryService lazily owns independent raw-history ledgers for inspection consumers, loading the current tail first and prepending one older page only when its consumer requests it. Each history snapshot exposes the raw window's absolute base sequence so a consumer detects a prepend even when the page adds no surface-visible node. WorkspacesService depends on SessionsService and owns Workspace objects, list/actions, default-target derivation, and the New Session blank-reuse entry (`connectWorkspace`). The runtime fans the shared Host stream into the Session, Workspace, and activated history owners without routing inspection state through Session or SessionManager, and bridges the registry-invalidation frames to typed ctx events (`commands/changed`, `session/preset-changed`, `settings/changed`, `credentials/changed`, `models/changed`) so surface caches refetch without touching the stream. `host/session-preset-changed` also folds its preset into the session row, because the switch's RPC echo reaches only the client that issued it. Client sessions are always Host-born (Session+Agent+cwd in one `session.create`); the client holds no pre-entity session state — a session's Agent scope (the client mirror of host dsh-scope, keyed by the shared agent/session id) is born when its row enters the list mirror and dies with the prune. Each `Session` holds a generic `ProjectionValueStore` seeded from the history-tail `projections` block and updated by `session/projection` frames under higher-seq-wins; domain keys (including `todos`) are read via `projections.faceOf` / `useProjection`, not via `ConversationSnapshot`. The store also publishes one reference-stable whole-value map through `SessionSummary.projectionValues`, allowing global list consumers to reuse the same projections without creating per-session subscriptions.
|
||||
Client cordis boot and React-free object services: SlotsService wraps SlotCore and supplies renderer data sources; SessionsService owns Session objects, list and scope state, and the shared event window and history paging used by registered conversation view targets. WorkspacesService depends on SessionsService and owns Workspace objects, list/actions, default-target derivation, and the New Session blank-reuse entry (`connectWorkspace`). The runtime fans the shared Host stream into Session and Workspace owners and bridges the registry-invalidation frames to typed ctx events (`commands/changed`, `session/preset-changed`, `settings/changed`, `credentials/changed`, `models/changed`) so surface caches refetch without touching the stream. `host/session-preset-changed` also folds its preset into the session row, because the switch's RPC echo reaches only the client that issued it. Client sessions are always Host-born (Session+Agent+cwd in one `session.create`); the client holds no pre-entity session state — a session's Agent scope (the client mirror of host dsh-scope, keyed by the shared agent/session id) is born when its row enters the list mirror and dies with the prune. Contract: api-contracts v3 §4. Each `Session` holds a generic `ProjectionValueStore` seeded from the history-tail `projections` block and updated by `session/projection` frames under higher-seq-wins; domain keys (including `todos`) are read via `projections.faceOf` / `useProjection`, not via `ConversationSnapshot`. The store also publishes one reference-stable whole-value map through `SessionSummary.projectionValues`, allowing global list consumers to reuse the same projections without creating per-session subscriptions.
|
||||
|
||||
`bindSettingsScope` is the browser mirror of the Host-side settings owner seam for one domain-owned namespace. It subscribes before starting a nonblocking initial read, publishes a uSES snapshot (status, section value, revision, writability, host/memory mode), serializes `set` writes with the latest known namespace revision, suppresses stale publications, recovers a rejected latest write from Host state, and reaches quiescence on plugin disposal. The default decoder validates each section against the namespace's own serialized wire schema (rehydrated through dsh-client-schema-form), so a domain adds a decoder only to narrow beyond that schema. Loopback pages use the Host settings API; remote pages stay in memory mode. Domain packages own the namespace schema, default, and live service rather than putting product policy in runtime.
|
||||
|
||||
## Slot declaration injection
|
||||
|
||||
`ctx.slots.inject(name, callback)` makes a full `SlotMap` key the dependency for a contribution whose plugin can activate independently from the declaring entry. It runs `callback` synchronously when the declaration exists, otherwise waits; declaration collapse disposes the callback effect, and redeclaration reruns it. The controller belongs to the caller's plugin fiber, so unloading the contributor cancels either the wait or its active registrations. A direct `slots.register()` into an undeclared slot still throws.
|
||||
@@ -42,17 +41,17 @@ Each `Session` gives its contiguous event window to a `ConversationNodeAssembler
|
||||
|
||||
Definition authors keep matching local to the current event, give every correlated event a stable business id, and make updates replayable by log `seq`; renderers consume final Node data and constrained Location values rather than scanning Session or Chat collections. The [Conversation Node cookbook](../../../docs/cookbook/adding-a-conversation-node.md) gives the complete registration and pagination path.
|
||||
|
||||
`ui-conversation` registers the built-in Chat Definitions and the keyed Chat snapshot builder. Append-origin user, assistant, and Tool results remain the human record; model-only replacement copies stay out, except that a compaction checkpoint becomes its own marker and resolves missing summary provenance when an older page supplies it. Durable inbox splice Contexts classify next-step user messages as steering without making inbox state a Session special case. Context messages retain producer provenance and form. StatsLine reads `ConversationSnapshot.chat.legacy.nodes`, while Session mirrors that legacy slice into the top-level `nodes`, `partial`, and `runningCalls` public compatibility fields without running a second business fold. Trajectory consumes neither compatibility surface; its activated `session-history` inspection keeps an independent fold until it gains its own registered target.
|
||||
`ui-conversation` registers the built-in Chat Definitions and the keyed Chat snapshot builder. Append-origin user, assistant, and Tool results remain the human record; model-only replacement copies stay out, except that a compaction checkpoint becomes its own marker and resolves missing summary provenance when an older page supplies it. Durable inbox splice Contexts classify next-step user messages as steering without making inbox state a Session special case. Context messages retain producer provenance and form. StatsLine reads `ConversationSnapshot.chat.legacy.nodes`, while Session mirrors that legacy slice into the top-level `nodes`, `partial`, and `runningCalls` public compatibility fields without running a second business fold. `ui-trajectory` registers independent Definitions and a target builder over the same Session window; it preserves the existing stage-oriented view model without consuming the Chat compatibility fields or running another history fold.
|
||||
|
||||
The Chat builder keeps one mutable keyed store per Session. Content updates notify only the affected node key, structural changes rebuild order and Location membership, and a prepend adds rows without replacing existing keyed values. Assistant chunks update Definition State for every event but request at most one materialization per animation frame; final messages and Turn/Step closure publish immediately. See the [client Tool presentation decision](../../../.agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.md).
|
||||
|
||||
## Request inspection
|
||||
## Trajectory request data
|
||||
|
||||
`SessionHistoryInspection.requests` is one chronological, purpose-discriminated provider-request stream. Assistant requests always carry their numeric `turn` and `step`; compaction requests carry `step: 0` and a `turn` owner that may be `null`. That null owner means a manual compaction ran standalone between turns, not that it belongs to either adjacent turn. A `session/end-seed` boundary closes an unmatched compaction request as an error at the boundary time with `Compaction was interrupted before completion.`; a later start projects as an independent request instead of overwriting the orphan.
|
||||
Trajectory Definitions assemble one chronological, purpose-discriminated provider-request stream. Assistant requests always carry their numeric `turn` and `step`; compaction requests carry `step: 0` and a `turn` owner that may be `null`. That null owner means a manual compaction ran standalone between turns, not that it belongs to either adjacent turn. A `session/end-seed` boundary closes an unmatched compaction request as an error at the boundary time with `Compaction was interrupted before completion.`; a later start projects as an independent request instead of overwriting the orphan.
|
||||
|
||||
## Code Mode child-call tree
|
||||
|
||||
Every `ToolCallBlock` recursively owns its children through `subCalls`, in start order. Chat's Tool Definition correlates root calls and results by call id, folds Code Dispatch start/settlement records into that root Context, and projects one keyed recursive tree; child calls never become independent Chat roots. When a start falls outside the loaded window, its settlement remains renderable with `callTime: null`. A child update copies only its ancestor path, so unchanged siblings retain object identity. Edges that introduce a cycle or exceed the fixed 256-call depth limit are consumed without mutating the tree. The separate Trajectory history fold still uses Runtime's `ToolCallTree` over the same nested data contract.
|
||||
Every `ToolCallBlock` recursively owns its children through `subCalls`, in start order. Chat's Tool Definition correlates root calls and results by call id, folds Code Dispatch start/settlement records into that root Context, and projects one keyed recursive tree; child calls never become independent Chat roots. When a start falls outside the loaded window, its settlement remains renderable with `callTime: null`. A child update copies only its ancestor path, so unchanged siblings retain object identity. Edges that introduce a cycle or exceed the fixed 256-call depth limit are consumed without mutating the tree. Trajectory's Tool Definition independently assembles the same nested data contract for its target.
|
||||
|
||||
## Session title projection
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
客户端 cordis 启动与不依赖 React 的对象服务:SlotsService 包装 SlotCore 并提供 renderer 数据源;SessionsService 拥有 Session 对象以及 Chat 所需的列表、scope 和事件窗口状态;SessionHistoryService 为检查类消费方惰性拥有彼此独立的原始历史账本,先加载当前尾部,并仅在消费方请求时向前补入一页更早历史。每份历史快照都会公开原始窗口的绝对基准序号,因此即使该页没有新增任何 surface 可见节点,消费方仍能检测到向前补页。WorkspacesService 依赖 SessionsService,拥有 Workspace 对象、列表/操作、默认目标派生,以及 New Session 空会话复用入口(`connectWorkspace`)。运行时把共享 Host 流分发给 Session、Workspace 和已激活的历史数据所有者,不让检查状态经过 Session 或 SessionManager,并把注册表失效帧桥接为类型化 ctx 事件(`commands/changed`、`session/preset-changed`、`settings/changed`、`credentials/changed`、`models/changed`),使各表面缓存无需触碰流即可重拉。`host/session-preset-changed` 还会把其中的 preset 折进会话行,因为这次切换的 RPC 回执只会到达发起它的那个客户端。客户端会话一律由 Host 创建(一次 `session.create` 同时产生 Session、agent(智能体)和 cwd);客户端不持有任何实体化之前的会话状态——agent scope(host dsh-scope 的客户端镜像,以 agent/session 共用 id 为键)在会话行进入列表镜像时创建,并随 prune 销毁。每个 `Session` 持有一个通用的 `ProjectionValueStore`,由历史记录尾部的 `projections` 块播种,并经 `session/projection` 帧按 seq 高者胜更新;领域键(含 `todos`)经 `projections.faceOf`/`useProjection` 读取,不经 `ConversationSnapshot`。该 store 还会通过 `SessionSummary.projectionValues` 发布一份引用稳定的完整值映射,使全局列表消费方无需为每个会话创建订阅,即可复用同一组投影。
|
||||
客户端 cordis 启动与不依赖 React 的对象服务:SlotsService 包装 SlotCore 并提供 renderer 数据源;SessionsService 拥有 Session 对象、列表与 scope 状态,以及供已注册 conversation view target 共用的事件窗口与历史分页。WorkspacesService 依赖 SessionsService,拥有 Workspace 对象、列表/操作、默认目标派生,以及 New Session 空会话复用入口(`connectWorkspace`)。运行时把共享 Host 流分发给 Session 与 Workspace 所有者,并把注册表失效帧桥接为类型化 ctx 事件(`commands/changed`、`session/preset-changed`、`settings/changed`、`credentials/changed`、`models/changed`),使各表面缓存无需触碰流即可重拉。`host/session-preset-changed` 还会把其中的 preset 折进会话行,因为这次切换的 RPC 回执只会到达发起它的那个客户端。客户端会话一律由 Host 创建(一次 `session.create` 同时产生 Session、agent(智能体)和 cwd);客户端不持有任何实体化之前的会话状态——agent scope(host dsh-scope 的客户端镜像,以 agent/session 共用 id 为键)在会话行进入列表镜像时创建,并随 prune 销毁。约定:api-contracts v3 §4。每个 `Session` 持有一个通用的 `ProjectionValueStore`,由历史记录尾部的 `projections` 块播种,并经 `session/projection` 帧按 seq 高者胜更新;领域键(含 `todos`)经 `projections.faceOf`/`useProjection` 读取,不经 `ConversationSnapshot`。该 store 还会通过 `SessionSummary.projectionValues` 发布一份引用稳定的完整值映射,使全局列表消费方无需为每个会话创建订阅,即可复用同一组投影。
|
||||
|
||||
`bindSettingsScope` 面向单个由领域持有的 namespace,是 Host 侧 settings owner seam 的浏览器镜像。它在开始非阻塞初始读取前建立订阅,发布 uSES 快照(状态、分节值、revision、可写性、host/内存模式),使用已知最新 namespace revision 串行执行 `set` 写入,抑制陈旧发布,并在最新写入被拒时从 Host 状态恢复;插件释放时,它会达到完全停稳。默认解码器会对照该 namespace 自身的序列化 wire schema(经 dsh-client-schema-form 还原)校验每个分节,因此领域只有在需要比该 schema 进一步收窄时才添加解码器。回环页面使用 Host settings API,远程页面则停留在内存模式。namespace schema、默认值与实时服务归领域包所有,而非把产品政策放入运行时。
|
||||
|
||||
## Slot 声明注入
|
||||
|
||||
`ctx.slots.inject(name, callback)` 将完整的 `SlotMap` key 作为贡献项的依赖,适用于贡献方插件可独立于声明条目激活的情形。声明存在时,它会同步运行 `callback`,否则等待;声明折叠会 dispose(资源释放)回调 effect,重新声明则会再次运行回调。控制器归调用方的插件 fiber 所有,因此卸载贡献方会取消等待或移除其活跃注册项。直接调用 `slots.register()` 向未声明 slot 注册仍会抛出异常。
|
||||
@@ -42,17 +41,17 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸
|
||||
|
||||
Definition 作者只根据当前事件完成匹配,为每条关联事件提供稳定业务 id,并保证 update 能按日志 `seq` 回放;renderer 只消费最终 Node data 与受限 Location value,不扫描 Session 或 Chat 集合。完整注册和分页路径见 [Conversation Node 实操手册](../../../docs/cookbook/adding-a-conversation-node.md)。
|
||||
|
||||
`ui-conversation` 注册内建 Chat Definition 与 keyed Chat snapshot builder。append 来源的 user、assistant 和 Tool result 构成人类可见记录;仅供模型使用的 replacement 副本不进入 Chat,compaction 检查点除外,它会成为独立标记,并在更早分页补齐 summary 溯源后更新。持久 inbox splice Context 能把 next-step 用户消息判定为 steering,无须让 inbox 状态成为 Session 特例。上下文消息保留生产者 provenance 与 form。StatsLine 读取 `ConversationSnapshot.chat.legacy.nodes`;Session 则把该 legacy slice 镜像到顶层 `nodes`、`partial` 和 `runningCalls` 公共兼容字段,无须运行第二套业务 fold。Trajectory 不消费这两种兼容表面;在它获得独立注册 target 之前,已激活的 `session-history` inspection 继续维护独立 fold。
|
||||
`ui-conversation` 注册内建 Chat Definition 与 keyed Chat snapshot builder。append 来源的 user、assistant 和 Tool result 构成人类可见记录;仅供模型使用的 replacement 副本不进入 Chat,compaction 检查点除外,它会成为独立标记,并在更早分页补齐 summary 溯源后更新。持久 inbox splice Context 能把 next-step 用户消息判定为 steering,无须让 inbox 状态成为 Session 特例。上下文消息保留生产者 provenance 与 form。StatsLine 读取 `ConversationSnapshot.chat.legacy.nodes`;Session 则把该 legacy slice 镜像到顶层 `nodes`、`partial` 和 `runningCalls` 公共兼容字段,无须运行第二套业务 fold。`ui-trajectory` 在同一个 Session 窗口上注册独立 Definition 与 target builder;它保留现有的 stage-oriented view model,既不消费 Chat 兼容字段,也不运行另一套 history fold。
|
||||
|
||||
Chat builder 为每个 Session 保留一个 mutable keyed store。内容更新只通知受影响的 node key;结构变化才重建顺序和 Location 成员关系;prepend 只增加行,不替换既有 keyed value。每个 Assistant chunk 都会更新 Definition State,但最多每个 animation frame 请求一次物化;final message 与 Turn/Step 关闭会立即发布。参见 [Client Tool 展示所有权决策](../../../.agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.md)。
|
||||
|
||||
## 请求检查
|
||||
## Trajectory 请求数据
|
||||
|
||||
`SessionHistoryInspection.requests` 是一条按时间顺序排列、以用途为判别字段的提供方请求流。助手请求始终携带数值型 `turn` 与 `step`;压缩请求携带 `step: 0`,其 `turn` 所有者可以是 `null`。这个 null 所有者表示手动压缩独立运行在两个轮次之间,并不表示它属于任一相邻轮次。`session/end-seed` 边界会在边界时刻将未匹配的压缩请求以错误状态结束,错误固定为 `Compaction was interrupted before completion.`;后续 start 会投影为独立请求,而不会覆盖这项遗留的未匹配请求。
|
||||
Trajectory Definition 组装出一条按时间顺序排列、以用途为判别字段的提供方请求流。助手请求始终携带数值型 `turn` 与 `step`;压缩请求携带 `step: 0`,其 `turn` 所有者可以是 `null`。这个 null 所有者表示手动压缩独立运行在两个轮次之间,并不表示它属于任一相邻轮次。`session/end-seed` 边界会在边界时刻将未匹配的压缩请求以错误状态结束,错误固定为 `Compaction was interrupted before completion.`;后续 start 会投影为独立请求,而不会覆盖这项遗留的未匹配请求。
|
||||
|
||||
## Code Mode 子调用树
|
||||
|
||||
每个 `ToolCallBlock` 都通过 `subCalls` 按启动顺序递归拥有自己的子调用。Chat 的 Tool Definition 按 call id 关联 root call 与 result,把 Code Dispatch 的 start/settlement 记录折叠进该 root Context,并投影为一棵 keyed 递归树;child call 不会成为独立 Chat root。start 落在已加载窗口之外时,其 settlement 仍以 `callTime: null` 渲染。一次 child 更新只复制其祖先链,因此未变化的 sibling 保持对象身份。会引入环或超过固定 256 层深度上限的边会被消费,但不会修改树。独立的 Trajectory history fold 仍通过 Runtime 的 `ToolCallTree` 生成同一种嵌套数据契约。
|
||||
每个 `ToolCallBlock` 都通过 `subCalls` 按启动顺序递归拥有自己的子调用。Chat 的 Tool Definition 按 call id 关联 root call 与 result,把 Code Dispatch 的 start/settlement 记录折叠进该 root Context,并投影为一棵 keyed 递归树;child call 不会成为独立 Chat root。start 落在已加载窗口之外时,其 settlement 仍以 `callTime: null` 渲染。一次 child 更新只复制其祖先链,因此未变化的 sibling 保持对象身份。会引入环或超过固定 256 层深度上限的边会被消费,但不会修改树。Trajectory 的 Tool Definition 为自己的 target 独立组装同一种嵌套数据契约。
|
||||
|
||||
## Session 标题投影
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-runtime",
|
||||
"description": "Client core services: SlotsService, SessionsService (scope tree + object layer)",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/runtime"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -39,7 +46,6 @@
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-schema-form": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-compact": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
@@ -53,10 +59,10 @@
|
||||
"zustand": "~4.4.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-type-meta": "^0.0.1",
|
||||
"@deepseek-ai/dsh-typert-registry": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-type-meta": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
@@ -64,8 +70,8 @@
|
||||
"@deepseek-ai/dsh-type-meta": "workspace:^",
|
||||
"@deepseek-ai/dsh-typert-registry": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -110,6 +110,17 @@ export interface ConversationViewNode {
|
||||
readonly data: unknown
|
||||
}
|
||||
|
||||
/** Merge-extensible immutable snapshots published by registered view targets. */
|
||||
export interface ConversationViewSnapshotMap {}
|
||||
|
||||
/** Stable reader over the latest snapshot of every registered view target. */
|
||||
export interface ConversationViewSnapshotStore {
|
||||
/** @param target - registered view target. @returns its current snapshot. */
|
||||
get<Target extends Extract<keyof ConversationViewSnapshotMap, string>>(
|
||||
target: Target,
|
||||
): ConversationViewSnapshotMap[Target] | undefined
|
||||
}
|
||||
|
||||
/** Final Chat render unit produced directly by a business Definition. */
|
||||
export interface ChatConversationViewNode extends ConversationViewNode {
|
||||
readonly target: 'chat'
|
||||
@@ -159,6 +170,8 @@ export type ConversationLocationDataScope = 'step' | 'turn'
|
||||
/** One independently registered business Event-to-Node state machine. */
|
||||
export interface ConversationNodeDefinition<State = unknown> {
|
||||
readonly kind: string
|
||||
/** Sole view target owned by this Definition; omitted for state-only Contexts. */
|
||||
readonly target?: string
|
||||
/**
|
||||
* Extract this Definition's stable business identity from one event.
|
||||
* @param event - raw Session event; no Context or history access is available.
|
||||
@@ -207,15 +220,11 @@ export interface ConversationNodeDefinition<State = unknown> {
|
||||
scope: ConversationLocationDataScope,
|
||||
): ConversationLocationData | null
|
||||
/**
|
||||
* Materialize one final Node for a registered view target.
|
||||
* Materialize one final Node for this Definition's declared view target.
|
||||
* @param context - latest complete Context.
|
||||
* @param target - registered view target such as `chat`.
|
||||
* @returns final Node, or null when this Context is not currently visible.
|
||||
*/
|
||||
buildViewNode(
|
||||
context: ConversationNodeContext<State>,
|
||||
target: string,
|
||||
): ConversationViewNode | null
|
||||
buildViewNode?(context: ConversationNodeContext<State>): ConversationViewNode | null
|
||||
}
|
||||
|
||||
/** Reference-stable Turn/Step facts published beside view Nodes. */
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import type {
|
||||
RpcError, SessionId,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionHistoryInspection } from '../sessions/history.ts'
|
||||
import type { ObservableSnapshot } from './store.ts'
|
||||
|
||||
/** Observable state of one independently loaded session history ledger. */
|
||||
export interface SessionHistorySnapshot {
|
||||
state: 'cold' | 'loading' | 'ready' | 'error'
|
||||
error: RpcError | null
|
||||
hasMore: boolean
|
||||
/** Absolute sequence of the first loaded raw event, or zero for an empty window. */
|
||||
baseSeq: number
|
||||
inspection: SessionHistoryInspection
|
||||
}
|
||||
|
||||
/** Read-only history source addressed by session id. */
|
||||
export interface SessionHistoryFace
|
||||
extends ObservableSnapshot<SessionHistorySnapshot> {
|
||||
readonly sessionId: SessionId
|
||||
/**
|
||||
* Load the current tail without reading older pages.
|
||||
* @param signal - Consumer lifetime.
|
||||
* @returns When the tail is ready or loading fails.
|
||||
*/
|
||||
loadTail(signal?: AbortSignal): Promise<void>
|
||||
/**
|
||||
* Prepend one older page when the current window has a predecessor.
|
||||
* @param signal - Consumer lifetime.
|
||||
* @returns Whether the loaded window advanced.
|
||||
*/
|
||||
loadOlder(signal?: AbortSignal): Promise<boolean>
|
||||
}
|
||||
|
||||
/** Runtime service resolving independent history sources. */
|
||||
export interface ISessionHistory {
|
||||
/**
|
||||
* Resolve the identity-stable source for a session.
|
||||
* @param sessionId - Host session identity.
|
||||
* @returns The source owned outside Session and SessionManager.
|
||||
*/
|
||||
source(sessionId: SessionId): SessionHistoryFace
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export class ConversationEventRegistry extends ConversationDefinitionRegistry<Co
|
||||
* @returns idempotent disposer.
|
||||
*/
|
||||
register(definition: ConversationNodeDefinition): () => void {
|
||||
assertDefinitionTarget(definition)
|
||||
return this.registerDefinition(
|
||||
definition.kind,
|
||||
definition,
|
||||
@@ -31,6 +32,9 @@ export class ConversationEventRegistry extends ConversationDefinitionRegistry<Co
|
||||
* @returns idempotent disposer.
|
||||
*/
|
||||
registerFallback(definition: ConversationNodeDefinition): () => void {
|
||||
assertDefinitionTarget(definition)
|
||||
const target = definition.target
|
||||
if (target === undefined) throw new Error('conversation fallback Definition must declare a target')
|
||||
if (this.fallback !== undefined) throw new Error('conversation fallback Definition is already registered')
|
||||
const owner = this.ctx
|
||||
const dispose = owner.effect(() => {
|
||||
@@ -52,5 +56,12 @@ export class ConversationEventRegistry extends ConversationDefinitionRegistry<Co
|
||||
fallbackEntry(): ConversationNodeDefinition | undefined {
|
||||
return this.fallback
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function assertDefinitionTarget(definition: ConversationNodeDefinition): void {
|
||||
if ((definition.target === undefined) !== (definition.buildViewNode === undefined)) {
|
||||
throw new Error(
|
||||
`conversation Definition "${definition.kind}" must declare target and buildViewNode together`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { MaybeSnapshotSelectorHook, SnapshotSelectorHook } from '@deepseek-
|
||||
import { SlotsService } from './slots.ts'
|
||||
import { SessionsService } from './sessions/service.ts'
|
||||
import type { SessionListState } from './sessions/service.ts'
|
||||
import { SessionHistoryService } from './session-history/service.ts'
|
||||
import { WorkspacesService } from './workspaces/service.ts'
|
||||
import type { ConversationSnapshot } from './sessions/conversation.ts'
|
||||
import type { UseProjection } from './sessions/projection-store.ts'
|
||||
@@ -28,12 +27,12 @@ export type {
|
||||
ConversationLocation, ConversationMatch, ConversationMatchResult,
|
||||
ConversationNodeContext, ConversationNodeDefinition, ConversationPreviousContext,
|
||||
ConversationPublication, ConversationTimelineSnapshot, ConversationTurnDataMap, ConversationViewBuilder,
|
||||
ConversationViewDefinition, ConversationViewNode, StepLocation, TurnLocation,
|
||||
ConversationViewDefinition, ConversationViewNode, ConversationViewSnapshotMap,
|
||||
ConversationViewSnapshotStore, StepLocation, TurnLocation,
|
||||
} from './contract/conversation.ts'
|
||||
export type { ConversationRuntime } from './sessions/conversation-assembler.ts'
|
||||
export type { RootOwnerProps } from './slots.ts'
|
||||
export { SessionCreateError, SessionsService, scopeOf, workspaceTitleOf } from './sessions/service.ts'
|
||||
export { SessionHistoryService } from './session-history/service.ts'
|
||||
export { indexSubagentDescendants } from './sessions/subagent-lineage.ts'
|
||||
export type { SubagentDescendantSummary } from './sessions/subagent-lineage.ts'
|
||||
// The provide channel is shared with the client test runtime (one
|
||||
@@ -48,9 +47,6 @@ export type { SettingsScope, SettingsScopeSnapshot, SettingsScopeSpec } from './
|
||||
export { resolveWorkspacePath } from './workspaces/path.ts'
|
||||
export type { Session } from './sessions/session.ts'
|
||||
export type { ISession, ProjectionsFace, SessionFace } from './contract/session.ts'
|
||||
export type {
|
||||
ISessionHistory, SessionHistoryFace, SessionHistorySnapshot,
|
||||
} from './contract/session-history.ts'
|
||||
export type { AgentContext, ISessions } from './contract/sessions.ts'
|
||||
export type { IWorkspaces } from './contract/workspaces.ts'
|
||||
export type {
|
||||
@@ -76,7 +72,9 @@ export type {
|
||||
LegacyConversationSlice, PartialAssistant, RunningToolCall,
|
||||
SteeringMessageNode, TodoItem, ToolCallBlock, ToolResultNode, TurnErrorNode, UnknownSurfaceNode, UserMessageNode,
|
||||
} from './sessions/conversation.ts'
|
||||
export { EMPTY_CHAT_SNAPSHOT, toAssistantBlock, toAssistantBlocks } from './sessions/conversation.ts'
|
||||
export {
|
||||
EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS, toAssistantBlock, toAssistantBlocks,
|
||||
} from './sessions/conversation.ts'
|
||||
export { emptyAssistantBlock } from './sessions/partial.ts'
|
||||
export { isTokenDelta } from './sessions/assistant-timing.ts'
|
||||
export { contextForm, contextProvenance } from './sessions/context-provenance.ts'
|
||||
@@ -90,8 +88,6 @@ export type {
|
||||
export type {
|
||||
ConversationPromptSnapshot, RequestInspectionSnapshot, RequestPromptChange, RequestView,
|
||||
} from './sessions/request-inspection.ts'
|
||||
export type { ConversationHistoryProjection } from './session-history/history-fold.ts'
|
||||
export type { SessionHistoryInspection } from './sessions/history.ts'
|
||||
export { PendingWait } from './sessions/pending.ts'
|
||||
export type {
|
||||
PendingInteraction, PendingInteractionStatus, PendingKind, PendingPayloads,
|
||||
@@ -211,8 +207,6 @@ declare module '@deepseek-ai/cordis' {
|
||||
conversationViews: import('./conversation/view-registry.ts').ConversationViewRegistry
|
||||
/** The outward face only; the concrete service stays inside the runtime. */
|
||||
sessions: import('./contract/sessions.ts').ISessions
|
||||
/** Read-only history sources isolated from Chat sessions and workspace state. */
|
||||
sessionHistory: import('./contract/session-history.ts').ISessionHistory
|
||||
/** The outward face only; the concrete service stays inside the runtime. */
|
||||
workspaces: import('./contract/workspaces.ts').IWorkspaces
|
||||
}
|
||||
@@ -235,7 +229,6 @@ export function apply(ctx: Context): void {
|
||||
ctx.typert.contexts.registerClient('agent', {
|
||||
identity: candidate => sessions.scopeOf(candidate),
|
||||
})
|
||||
const sessionHistory = new SessionHistoryService(ctx, connection.api)
|
||||
const workspaces = new WorkspacesService(ctx, connection.api, sessions)
|
||||
ctx.effect(
|
||||
() => workspaces.startInitialSelection(),
|
||||
@@ -244,11 +237,6 @@ export function apply(ctx: Context): void {
|
||||
const loop = connection.start({
|
||||
onMuxEnvelope: (envelope) => {
|
||||
sessions.handleMuxEnvelope(envelope)
|
||||
try {
|
||||
sessionHistory.handleMuxEnvelope(envelope)
|
||||
} catch (error) {
|
||||
console.error('[web-runtime] history frame routing failed:', error)
|
||||
}
|
||||
},
|
||||
onHostEnvelope: (envelope) => {
|
||||
sessions.handleHostEnvelope(envelope)
|
||||
@@ -264,21 +252,11 @@ export function apply(ctx: Context): void {
|
||||
else if (frame.type === 'host/settings-changed') ctx.emit('settings/changed', frame.ns)
|
||||
else if (frame.type === 'host/credentials-changed') ctx.emit('credentials/changed', frame.ref)
|
||||
else if (frame.type === 'host/models-changed') ctx.emit('models/changed')
|
||||
try {
|
||||
sessionHistory.handleHostEnvelope(envelope)
|
||||
} catch (error) {
|
||||
console.error('[web-runtime] history host-frame routing failed:', error)
|
||||
}
|
||||
},
|
||||
onConnected: () => {
|
||||
sessions.handleConnected()
|
||||
workspaces.handleConnected()
|
||||
ctx.emit('connection/reset')
|
||||
try {
|
||||
sessionHistory.handleConnected()
|
||||
} catch (error) {
|
||||
console.error('[web-runtime] history reconnect failed:', error)
|
||||
}
|
||||
},
|
||||
onStateChange: (state) => {
|
||||
// Generation death fires before any next-generation frame can arrive
|
||||
@@ -286,11 +264,6 @@ export function apply(ctx: Context): void {
|
||||
// the only safe moment to drop generation-scoped interaction state.
|
||||
if (state === 'reconnecting') {
|
||||
sessions.handleDisconnected()
|
||||
try {
|
||||
sessionHistory.handleDisconnected()
|
||||
} catch (error) {
|
||||
console.error('[web-runtime] history disconnect failed:', error)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,428 +0,0 @@
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import {
|
||||
SurfaceManager, isSurfaceEligibleType, isSurfaceEvent,
|
||||
} from '@deepseek-ai/dsh-session/surface'
|
||||
import type {
|
||||
HistoryEntry, ToolCallView, ToolResultView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type {
|
||||
AssistantRequestConfig, AssistantTiming, ConversationNode,
|
||||
PartialAssistant, RunningToolCall,
|
||||
} from '../sessions/conversation.ts'
|
||||
import { toAssistantBlocks } from '../sessions/conversation.ts'
|
||||
import { contextForm, contextProvenance } from '../sessions/context-provenance.ts'
|
||||
import { SteeringHistory } from '../sessions/steering-history.ts'
|
||||
import type {
|
||||
ConversationContext, ConversationContextOriginKind,
|
||||
} from '../sessions/conversation-context.ts'
|
||||
import type { ConversationPromptSnapshot } from '../sessions/request-inspection.ts'
|
||||
import { PartialAccumulator } from '../sessions/partial.ts'
|
||||
import type { AssistantStepMetadata } from '../sessions/assistant-timing.ts'
|
||||
import { indexAssistantStepTiming, settledAssistantTiming } from '../sessions/assistant-timing.ts'
|
||||
import { ToolCallTree } from '../sessions/tool-call-tree.ts'
|
||||
|
||||
interface CallIndexEntry {
|
||||
name: string
|
||||
argsRaw: string
|
||||
time: number
|
||||
callView: ToolCallView | null
|
||||
}
|
||||
|
||||
interface FoldedContext {
|
||||
generation: number
|
||||
nodes: readonly number[]
|
||||
originSeq?: number
|
||||
}
|
||||
|
||||
/** Immutable conversation projections derived only from the history source. */
|
||||
export interface ConversationHistoryProjection {
|
||||
eventNodes: readonly ConversationNode[]
|
||||
contexts: readonly ConversationContext[]
|
||||
interruptedNodes: readonly ConversationNode[]
|
||||
partial: PartialAssistant | null
|
||||
runningCalls: readonly RunningToolCall[]
|
||||
}
|
||||
|
||||
function replacementCrossesWindowHead(event: SessionEvent, baseSeq: number): boolean {
|
||||
if (!isSurfaceEvent(event) || event.surfaceOp === 'append') return false
|
||||
return event.surfaceOp.start < baseSeq || event.surfaceOp.end < baseSeq
|
||||
}
|
||||
|
||||
function contextOriginKind(event: SessionEvent | undefined): ConversationContextOriginKind {
|
||||
if (event?.type !== 'user/message') return 'rewrite'
|
||||
const source = event.data.source
|
||||
if (typeof source === 'object' && 'kind' in source && 'plugin' in source) {
|
||||
if (source.plugin === 'compact') return 'compaction'
|
||||
if (source.plugin === 'rewind') return 'rewind'
|
||||
}
|
||||
return 'rewrite'
|
||||
}
|
||||
|
||||
function foldContexts(events: readonly SessionEvent[]): readonly FoldedContext[] {
|
||||
const replay: SessionEvent[] = []
|
||||
const originalSeqs: number[] = []
|
||||
const rebasedSeqByOriginal = new Map<number, number>()
|
||||
const surface = new SurfaceManager(replay)
|
||||
const contexts: FoldedContext[] = []
|
||||
let generation = 0
|
||||
let originSeq: number | undefined
|
||||
const originalNodes = () => surface.nodes.map((seq) => {
|
||||
const original = originalSeqs[seq]
|
||||
if (original === undefined) throw new Error(`rebased surface seq ${seq} has no origin`)
|
||||
return original
|
||||
})
|
||||
for (const event of events) {
|
||||
if (!isSurfaceEvent(event)) continue
|
||||
if (event.surfaceOp !== 'append') {
|
||||
contexts.push({
|
||||
generation,
|
||||
nodes: originalNodes(),
|
||||
...(originSeq === undefined ? {} : { originSeq }),
|
||||
})
|
||||
generation++
|
||||
originSeq = event.seq
|
||||
}
|
||||
const rebasedSeq = replay.length
|
||||
const {
|
||||
sourceEventSeqs: rawSources,
|
||||
...eventWithoutSources
|
||||
} = event as SessionEvent & { sourceEventSeqs?: readonly number[] }
|
||||
const mappedSourceEventSeqs = rawSources?.flatMap((seq) => {
|
||||
const rebased = rebasedSeqByOriginal.get(seq)
|
||||
return rebased === undefined ? [] : [rebased]
|
||||
})
|
||||
const sourceEventSeqs = mappedSourceEventSeqs?.length === 0
|
||||
? undefined
|
||||
: mappedSourceEventSeqs
|
||||
const surfaceOp = event.surfaceOp === 'append'
|
||||
? event.surfaceOp
|
||||
: {
|
||||
...event.surfaceOp,
|
||||
start: rebasedSeqByOriginal.get(event.surfaceOp.start) ?? event.surfaceOp.start,
|
||||
end: rebasedSeqByOriginal.get(event.surfaceOp.end) ?? event.surfaceOp.end,
|
||||
}
|
||||
originalSeqs.push(event.seq)
|
||||
rebasedSeqByOriginal.set(event.seq, rebasedSeq)
|
||||
replay.push({
|
||||
...eventWithoutSources,
|
||||
seq: rebasedSeq,
|
||||
surfaceOp,
|
||||
...(sourceEventSeqs === undefined ? {} : { sourceEventSeqs }),
|
||||
} as SessionEvent)
|
||||
}
|
||||
contexts.push({
|
||||
generation,
|
||||
nodes: originalNodes(),
|
||||
...(originSeq === undefined ? {} : { originSeq }),
|
||||
})
|
||||
return contexts
|
||||
}
|
||||
|
||||
// History projection owns its node mapping so Chat's live adapter remains free
|
||||
// of inspection metadata and lifecycle coupling.
|
||||
/* jscpd:ignore-start */
|
||||
function materializeNode(
|
||||
event: SessionEvent,
|
||||
callIndex: ReadonlyMap<string, CallIndexEntry>,
|
||||
resultView: ToolResultView | null,
|
||||
assistantTiming: AssistantTiming | undefined,
|
||||
requestConfig: AssistantRequestConfig | undefined,
|
||||
steering: boolean,
|
||||
): ConversationNode {
|
||||
switch (event.type) {
|
||||
case 'user/message':
|
||||
if (event.data.source.kind !== 'user') {
|
||||
return {
|
||||
kind: 'context', seq: event.seq, time: event.time,
|
||||
content: event.data.content, source: event.data.source,
|
||||
provenance: contextProvenance(event.data.source),
|
||||
form: contextForm(event.data.source),
|
||||
}
|
||||
}
|
||||
if (steering) {
|
||||
return {
|
||||
kind: 'steering', messageId: event.data.id,
|
||||
seq: event.seq, time: event.time,
|
||||
content: event.data.content, source: event.data.source,
|
||||
}
|
||||
}
|
||||
return {
|
||||
kind: 'user', seq: event.seq, time: event.time,
|
||||
content: event.data.content, source: event.data.source,
|
||||
}
|
||||
case 'assistant/message':
|
||||
return {
|
||||
kind: 'assistant', seq: event.seq, time: event.time,
|
||||
turn: event.data.turn, step: event.data.step,
|
||||
blocks: toAssistantBlocks(event.data.message.content), usage: event.data.usage,
|
||||
provenance: {
|
||||
provider: event.data.message.source.provider,
|
||||
model: event.data.message.source.model,
|
||||
},
|
||||
...(requestConfig === undefined ? {} : { requestConfig }),
|
||||
...(assistantTiming === undefined ? {} : { timing: assistantTiming }),
|
||||
}
|
||||
case 'tool/result': {
|
||||
const result = event.data.message.content[0]
|
||||
const callId = String(event.data.message.source.callId)
|
||||
const call = callIndex.get(callId)
|
||||
return {
|
||||
kind: 'tool-result', seq: event.seq, time: event.time,
|
||||
callId,
|
||||
call: call === undefined ? null : { name: call.name, argsRaw: call.argsRaw },
|
||||
callTime: call?.time ?? null,
|
||||
content: result.content, isError: result.isError === true,
|
||||
...(event.data.error === undefined ? {} : { error: event.data.error }),
|
||||
meta: event.data.meta,
|
||||
callView: call?.callView ?? null,
|
||||
resultView,
|
||||
subCalls: [],
|
||||
}
|
||||
}
|
||||
default:
|
||||
return {
|
||||
kind: 'unknown', seq: event.seq, time: event.time,
|
||||
type: event.type, data: (event as { data?: unknown }).data,
|
||||
}
|
||||
}
|
||||
}
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
interface TransientProjection extends Pick<
|
||||
ConversationHistoryProjection,
|
||||
'interruptedNodes' | 'partial' | 'runningCalls'
|
||||
> {
|
||||
toolCallTree: ToolCallTree
|
||||
}
|
||||
|
||||
function projectTransient(entries: readonly HistoryEntry[]): TransientProjection {
|
||||
let partial: PartialAccumulator | null = null
|
||||
const openCalls = new Map<string, RunningToolCall>()
|
||||
const interruptedNodes: ConversationNode[] = []
|
||||
const toolCallTree = new ToolCallTree()
|
||||
|
||||
for (const entry of entries) {
|
||||
const { event } = entry
|
||||
if (toolCallTree.apply(event)) continue
|
||||
switch (event.type) {
|
||||
case 'assistant/chunk': {
|
||||
const { turn, step, chunk } = event.data
|
||||
if (partial === null || partial.turn !== turn || partial.step !== step) {
|
||||
partial = new PartialAccumulator(turn, step)
|
||||
}
|
||||
partial.push(chunk)
|
||||
break
|
||||
}
|
||||
case 'assistant/message':
|
||||
if (partial?.turn === event.data.turn && partial.step === event.data.step) partial = null
|
||||
break
|
||||
case 'tool/call':
|
||||
// History reconstructs its own in-flight index; this intentionally
|
||||
// mirrors the published Chat node shape, not Chat's mutable state.
|
||||
/* jscpd:ignore-start */
|
||||
openCalls.set(String(event.data.callId), {
|
||||
callId: String(event.data.callId),
|
||||
name: event.data.name,
|
||||
argsRaw: event.data.arguments,
|
||||
turn: event.data.turn,
|
||||
step: event.data.step,
|
||||
time: event.time,
|
||||
callView: entry.view?.for === 'call' ? entry.view.view : null,
|
||||
subCalls: [],
|
||||
})
|
||||
/* jscpd:ignore-end */
|
||||
break
|
||||
case 'tool/result':
|
||||
openCalls.delete(String(event.data.message.source.callId))
|
||||
break
|
||||
case 'turn/end': {
|
||||
if (partial !== null && partial.turn === event.data.turn) {
|
||||
const { blocks } = partial.toPartial()
|
||||
const visible = blocks.some(block =>
|
||||
block.kind === 'text' || block.kind === 'reasoning' ? block.text !== '' : true)
|
||||
if (visible) {
|
||||
interruptedNodes.push({
|
||||
kind: 'assistant', seq: event.seq - 0.9, time: event.time,
|
||||
turn: partial.turn, step: partial.step, blocks, interrupted: true,
|
||||
})
|
||||
}
|
||||
partial = null
|
||||
}
|
||||
let callOffset = 0
|
||||
for (const [callId, call] of openCalls) {
|
||||
if (call.turn !== event.data.turn) continue
|
||||
openCalls.delete(callId)
|
||||
// Interrupted terminal nodes are reconstructed independently so a
|
||||
// Trajectory replay cannot observe Session's frozen-node lifecycle.
|
||||
/* jscpd:ignore-start */
|
||||
interruptedNodes.push({
|
||||
kind: 'tool-result', seq: event.seq - 0.8 + callOffset++ * 0.01,
|
||||
time: event.time,
|
||||
callId,
|
||||
call: { name: call.name, argsRaw: call.argsRaw },
|
||||
callTime: call.time,
|
||||
content: [],
|
||||
isError: true,
|
||||
error: { name: 'Interrupted', code: 'interrupted' },
|
||||
callView: call.callView,
|
||||
resultView: null,
|
||||
subCalls: [],
|
||||
})
|
||||
/* jscpd:ignore-end */
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
interruptedNodes,
|
||||
partial: partial?.toPartial() ?? null,
|
||||
runningCalls: [...openCalls.values()],
|
||||
toolCallTree,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Project one immutable history ledger without reading or mutating Chat state.
|
||||
* @param entries - Contiguous history entries in sequence order.
|
||||
* @returns Event order, context lineage, and transient tail state.
|
||||
*/
|
||||
export function projectConversationHistory(
|
||||
entries: readonly HistoryEntry[],
|
||||
): ConversationHistoryProjection {
|
||||
const events = entries.map(entry => entry.event)
|
||||
const steeringHistory = new SteeringHistory()
|
||||
const steeringSeqs = new Set<number>()
|
||||
for (const event of events) {
|
||||
if (steeringHistory.apply(event)) steeringSeqs.add(event.seq)
|
||||
}
|
||||
const baseSeq = events[0]?.seq ?? 0
|
||||
const eventsBySeq = new Map(events.map(event => [event.seq, event]))
|
||||
const callIndex = new Map<string, CallIndexEntry>()
|
||||
const resultViews = new Map<number, ToolResultView>()
|
||||
const assistantSteps = new Map<string, AssistantStepMetadata>()
|
||||
const assistantTimings = new Map<number, AssistantTiming>()
|
||||
const assistantRequestConfigs = new Map<number, AssistantRequestConfig>()
|
||||
const promptsByContext = new Map<number, ConversationPromptSnapshot>()
|
||||
let activeRequestConfig: AssistantRequestConfig | undefined
|
||||
let activePrompt: ConversationPromptSnapshot | undefined
|
||||
let contextGeneration = 0
|
||||
|
||||
for (const [index, event] of events.entries()) {
|
||||
const view = entries[index]?.view
|
||||
if (event.type === 'tool/call') {
|
||||
callIndex.set(String(event.data.callId), {
|
||||
name: event.data.name,
|
||||
argsRaw: event.data.arguments,
|
||||
time: event.time,
|
||||
callView: view?.for === 'call' ? view.view : null,
|
||||
})
|
||||
} else if (event.type === 'tool/result' && view?.for === 'result') {
|
||||
resultViews.set(event.seq, view.view)
|
||||
}
|
||||
if (isSurfaceEvent(event) && event.surfaceOp !== 'append') {
|
||||
contextGeneration++
|
||||
if (activePrompt !== undefined) promptsByContext.set(contextGeneration, activePrompt)
|
||||
}
|
||||
indexAssistantStepTiming(assistantSteps, event)
|
||||
if (event.type === 'request/header') {
|
||||
activeRequestConfig = event.data.header.config
|
||||
activePrompt = {
|
||||
config: event.data.header.config,
|
||||
system: event.data.header.system ?? '',
|
||||
tools: event.data.header.tools ?? [],
|
||||
}
|
||||
promptsByContext.set(contextGeneration, activePrompt)
|
||||
} else if (event.type === 'assistant/message') {
|
||||
assistantTimings.set(
|
||||
event.seq,
|
||||
settledAssistantTiming(assistantSteps, event.data.turn, event.data.step, event.time),
|
||||
)
|
||||
if (activeRequestConfig !== undefined) {
|
||||
assistantRequestConfigs.set(event.seq, activeRequestConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nodeCache = new Map<number, ConversationNode>()
|
||||
const materialize = (seq: number): ConversationNode | undefined => {
|
||||
const cached = nodeCache.get(seq)
|
||||
if (cached !== undefined) return cached
|
||||
const event = eventsBySeq.get(seq)
|
||||
if (event === undefined || !isSurfaceEligibleType(event.type)) return
|
||||
const node = materializeNode(
|
||||
event,
|
||||
callIndex,
|
||||
resultViews.get(seq) ?? null,
|
||||
assistantTimings.get(seq),
|
||||
assistantRequestConfigs.get(seq),
|
||||
steeringSeqs.has(seq),
|
||||
)
|
||||
nodeCache.set(seq, node)
|
||||
return node
|
||||
}
|
||||
const eventNodes = events.flatMap((event) => {
|
||||
const node = materialize(event.seq)
|
||||
return node === undefined ? [] : [node]
|
||||
})
|
||||
|
||||
let contexts: readonly ConversationContext[]
|
||||
if (events.some(event => replacementCrossesWindowHead(event, baseSeq))) {
|
||||
contexts = [{
|
||||
id: 0,
|
||||
...(activePrompt === undefined ? {} : { prompt: activePrompt }),
|
||||
nodes: eventNodes,
|
||||
}]
|
||||
} else {
|
||||
try {
|
||||
contexts = foldContexts(events).map((context): ConversationContext => {
|
||||
const nodes = context.nodes.flatMap((seq) => {
|
||||
const node = materialize(seq)
|
||||
return node === undefined ? [] : [node]
|
||||
})
|
||||
const prompt = promptsByContext.get(context.generation)
|
||||
if (context.originSeq === undefined) {
|
||||
return {
|
||||
id: context.generation,
|
||||
...(prompt === undefined ? {} : { prompt }),
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
const originEvent = eventsBySeq.get(context.originSeq)
|
||||
return {
|
||||
id: context.generation,
|
||||
parentId: context.generation - 1,
|
||||
origin: contextOriginKind(originEvent),
|
||||
originSeq: context.originSeq,
|
||||
...(originEvent === undefined ? {} : { createdAt: originEvent.time }),
|
||||
...(prompt === undefined ? {} : { prompt }),
|
||||
nodes,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[web-runtime] history surface fold failed, using event order:', error)
|
||||
contexts = [{
|
||||
id: 0,
|
||||
...(activePrompt === undefined ? {} : { prompt: activePrompt }),
|
||||
nodes: eventNodes,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
const transient = projectTransient(entries)
|
||||
const projectedEventNodes = transient.toolCallTree.projectNodes(eventNodes)
|
||||
const projectedContexts = contexts.map((context): ConversationContext => {
|
||||
const nodes = transient.toolCallTree.projectNodes(context.nodes)
|
||||
return nodes === context.nodes ? context : { ...context, nodes }
|
||||
})
|
||||
return {
|
||||
eventNodes: projectedEventNodes,
|
||||
contexts: projectedContexts,
|
||||
interruptedNodes: transient.toolCallTree.projectNodes(transient.interruptedNodes),
|
||||
partial: transient.partial,
|
||||
runningCalls: transient.toolCallTree.projectRunningCalls(transient.runningCalls),
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {
|
||||
HostFrame, IApiClient, MuxFrame, RpcRequest, SessionId,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type {
|
||||
ISessionHistory, SessionHistoryFace,
|
||||
} from '../contract/session-history.ts'
|
||||
import { SessionHistorySource } from './source.ts'
|
||||
|
||||
/** Root registry and frame router for independent inspection histories. */
|
||||
export class SessionHistoryService implements ISessionHistory {
|
||||
private readonly sources = new Map<SessionId, SessionHistorySource>()
|
||||
|
||||
/**
|
||||
* @param ctx - Client root context.
|
||||
* @param api - Shared wire client.
|
||||
*/
|
||||
constructor(ctx: Context, private readonly api: IApiClient) {
|
||||
ctx.reflect.provide('sessionHistory', this, undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one identity-stable history source.
|
||||
* @param sessionId - Host session identity.
|
||||
* @returns Source independent from SessionManager.
|
||||
*/
|
||||
source(sessionId: SessionId): SessionHistoryFace {
|
||||
let source = this.sources.get(sessionId)
|
||||
if (source === undefined) {
|
||||
source = new SessionHistorySource(sessionId, this.api)
|
||||
this.sources.set(sessionId, source)
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
/**
|
||||
* Route history-relevant mux frames only to an existing source.
|
||||
* @param envelope - Validated mux envelope.
|
||||
*/
|
||||
handleMuxEnvelope(envelope: RpcRequest<MuxFrame>): void {
|
||||
const frame = envelope.payload
|
||||
if (frame.type === 'stream/error') return
|
||||
this.sources.get(frame.sessionId)?.handleMuxFrame(frame)
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a removed session's independent history source.
|
||||
* @param envelope - Validated host envelope.
|
||||
*/
|
||||
handleHostEnvelope(envelope: RpcRequest<HostFrame>): void {
|
||||
const frame = envelope.payload
|
||||
if (frame.type !== 'host/session-removed') return
|
||||
this.sources.get(frame.sessionId)?.dispose()
|
||||
this.sources.delete(frame.sessionId)
|
||||
}
|
||||
|
||||
/** Invalidate requests from the dead connection generation. */
|
||||
handleDisconnected(): void {
|
||||
for (const source of this.sources.values()) source.handleDisconnected()
|
||||
}
|
||||
|
||||
/** Rebuild every previously activated source from the new generation. */
|
||||
handleConnected(): void {
|
||||
for (const source of this.sources.values()) source.resync()
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
import type {
|
||||
HistoryEntry, IApiClient, MuxFrame, RpcError, SessionId,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import { transportError } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import type {
|
||||
SessionHistoryFace, SessionHistorySnapshot,
|
||||
} from '../contract/session-history.ts'
|
||||
import {
|
||||
compactHistoryInspectionEntries, createHistoryInspection,
|
||||
} from '../sessions/history.ts'
|
||||
import { Notifier } from '../sessions/notifier.ts'
|
||||
import { isVisibleAssistantChunk, PartialAccumulator } from '../sessions/partial.ts'
|
||||
|
||||
const HISTORY_PAGE_MESSAGES = 50
|
||||
|
||||
function isAborted(signal: AbortSignal | undefined): boolean {
|
||||
return signal?.aborted === true
|
||||
}
|
||||
|
||||
/** Independent raw-history owner used only by inspection consumers. */
|
||||
export class SessionHistorySource implements SessionHistoryFace {
|
||||
private entries: HistoryEntry[] = []
|
||||
private inspectionEntries: readonly HistoryEntry[] = []
|
||||
private baseSeq = 0
|
||||
private hasMore = false
|
||||
private state: SessionHistorySnapshot['state'] = 'cold'
|
||||
private error: RpcError | null = null
|
||||
private generation = 0
|
||||
private persistentConsumer = false
|
||||
private readonly consumerSignals = new Set<AbortSignal>()
|
||||
private openPromise: Promise<void> | null = null
|
||||
private olderPromise: Promise<void> | null = null
|
||||
private stitching = false
|
||||
private liveBuffer: HistoryEntry[] = []
|
||||
private subscribedLastSeq: number | null = null
|
||||
private inspectionCache: {
|
||||
entries: readonly HistoryEntry[]
|
||||
value: SessionHistorySnapshot['inspection']
|
||||
} | null = null
|
||||
private streamPublishToken: object | null = null
|
||||
private streamPartial: PartialAccumulator | null = null
|
||||
private snapshotCache: SessionHistorySnapshot
|
||||
private readonly notifier = new Notifier(() => {
|
||||
this.snapshotCache = this.buildSnapshot()
|
||||
})
|
||||
|
||||
/**
|
||||
* @param sessionId - Host session identity.
|
||||
* @param api - Shared wire client.
|
||||
*/
|
||||
constructor(
|
||||
readonly sessionId: SessionId,
|
||||
private readonly api: IApiClient,
|
||||
) {
|
||||
this.snapshotCache = this.buildSnapshot()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to ledger changes.
|
||||
* @param listener - Change callback.
|
||||
* @returns Unsubscribe function.
|
||||
*/
|
||||
subscribe(listener: () => void): () => void {
|
||||
return this.notifier.subscribe(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the cached ledger snapshot.
|
||||
* @returns Stable snapshot until the source changes.
|
||||
*/
|
||||
getSnapshot(): SessionHistorySnapshot {
|
||||
this.notifier.ensureFresh()
|
||||
return this.snapshotCache
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the current tail without reading older pages.
|
||||
* @param signal - Consumer lifetime.
|
||||
* @returns When the tail is ready or loading fails.
|
||||
*/
|
||||
async loadTail(signal?: AbortSignal): Promise<void> {
|
||||
if (isAborted(signal)) return
|
||||
this.trackConsumer(signal)
|
||||
await this.open()
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend one older page when the current window has a predecessor.
|
||||
* @param signal - Consumer lifetime.
|
||||
* @returns Whether the loaded window advanced.
|
||||
*/
|
||||
async loadOlder(signal?: AbortSignal): Promise<boolean> {
|
||||
if (isAborted(signal)) return false
|
||||
this.trackConsumer(signal)
|
||||
await this.open()
|
||||
if (isAborted(signal)) return false
|
||||
const previousBaseSeq = this.baseSeq
|
||||
await this.loadOlderPage()
|
||||
return this.baseSeq !== previousBaseSeq
|
||||
}
|
||||
|
||||
/**
|
||||
* Route a relevant mux frame without involving the Chat session.
|
||||
* @param frame - Session-addressed frame.
|
||||
*/
|
||||
handleMuxFrame(frame: MuxFrame): void {
|
||||
if (frame.type === 'session/subscribed') {
|
||||
this.subscribedLastSeq = frame.lastSeq
|
||||
return
|
||||
}
|
||||
if (frame.type !== 'session/event') return
|
||||
this.acceptLive({ event: frame.event, ...(frame.view === undefined ? {} : { view: frame.view }) })
|
||||
}
|
||||
|
||||
/** Invalidate dead-generation requests while retaining the last readable snapshot. */
|
||||
handleDisconnected(): void {
|
||||
this.generation++
|
||||
this.openPromise = null
|
||||
this.olderPromise = null
|
||||
this.stitching = false
|
||||
this.liveBuffer = []
|
||||
this.subscribedLastSeq = null
|
||||
if (this.state !== 'cold') {
|
||||
this.state = 'cold'
|
||||
this.error = null
|
||||
this.publishDirtyNow()
|
||||
}
|
||||
}
|
||||
|
||||
/** Rebuild an activated ledger from the new connection generation. */
|
||||
resync(): void {
|
||||
if (!this.hasConsumer()) return
|
||||
this.generation++
|
||||
this.openPromise = null
|
||||
this.olderPromise = null
|
||||
this.stitching = false
|
||||
this.liveBuffer = []
|
||||
this.subscribedLastSeq = null
|
||||
this.entries = []
|
||||
this.inspectionEntries = []
|
||||
this.baseSeq = 0
|
||||
this.hasMore = false
|
||||
this.state = 'cold'
|
||||
this.error = null
|
||||
this.publishDirtyNow()
|
||||
void this.open()
|
||||
}
|
||||
|
||||
/** Stop future refresh work after the host removes the session. */
|
||||
dispose(): void {
|
||||
this.persistentConsumer = false
|
||||
this.consumerSignals.clear()
|
||||
this.generation++
|
||||
this.openPromise = null
|
||||
this.olderPromise = null
|
||||
this.liveBuffer = []
|
||||
this.streamPublishToken = null
|
||||
this.streamPartial = null
|
||||
}
|
||||
|
||||
private open(): Promise<void> {
|
||||
if (this.state === 'ready') return Promise.resolve()
|
||||
if (this.openPromise !== null) return this.openPromise
|
||||
const generation = this.generation
|
||||
const operation = this.doOpen(generation)
|
||||
const settled = operation.finally(() => {
|
||||
if (this.openPromise === settled) this.openPromise = null
|
||||
})
|
||||
this.openPromise = settled
|
||||
return settled
|
||||
}
|
||||
|
||||
private trackConsumer(signal: AbortSignal | undefined): void {
|
||||
if (signal === undefined) {
|
||||
this.persistentConsumer = true
|
||||
return
|
||||
}
|
||||
if (this.consumerSignals.has(signal)) return
|
||||
this.consumerSignals.add(signal)
|
||||
signal.addEventListener('abort', () => {
|
||||
this.consumerSignals.delete(signal)
|
||||
}, { once: true })
|
||||
}
|
||||
|
||||
private hasConsumer(): boolean {
|
||||
return this.persistentConsumer || this.consumerSignals.size > 0
|
||||
}
|
||||
|
||||
private async doOpen(generation: number): Promise<void> {
|
||||
this.state = 'loading'
|
||||
this.error = null
|
||||
this.publishDirtyNow()
|
||||
try {
|
||||
let { result } = await this.api.sessions.history({
|
||||
sessionId: this.sessionId,
|
||||
maxMessages: HISTORY_PAGE_MESSAGES,
|
||||
})
|
||||
if (generation !== this.generation) return
|
||||
if (!result.ok) {
|
||||
this.state = 'error'
|
||||
this.error = result.error
|
||||
return
|
||||
}
|
||||
this.installTail(result.value.events, result.value.hasMore, true)
|
||||
const tailSeq = this.tailSeq()
|
||||
if (
|
||||
this.subscribedLastSeq !== null
|
||||
&& tailSeq !== null
|
||||
&& this.subscribedLastSeq > tailSeq
|
||||
) {
|
||||
result = (await this.api.sessions.history({
|
||||
sessionId: this.sessionId,
|
||||
maxMessages: HISTORY_PAGE_MESSAGES,
|
||||
})).result
|
||||
if (generation !== this.generation) return
|
||||
if (result.ok) this.installTail(result.value.events, result.value.hasMore, true)
|
||||
}
|
||||
this.state = 'ready'
|
||||
} catch (error) {
|
||||
if (generation !== this.generation) return
|
||||
this.state = 'error'
|
||||
const folded = transportError<never>(error)
|
||||
/* v8 ignore next -- transportError always returns the error branch. */
|
||||
this.error = folded.ok ? null : folded.error
|
||||
} finally {
|
||||
if (generation === this.generation) this.publishDirtyNow()
|
||||
}
|
||||
}
|
||||
|
||||
private loadOlderPage(): Promise<void> {
|
||||
if (this.olderPromise !== null) return this.olderPromise
|
||||
if (this.state !== 'ready' || !this.hasMore) return Promise.resolve()
|
||||
const generation = this.generation
|
||||
const operation = (async () => {
|
||||
try {
|
||||
const { result } = await this.api.sessions.history({
|
||||
sessionId: this.sessionId,
|
||||
beforeSeq: this.baseSeq,
|
||||
maxMessages: HISTORY_PAGE_MESSAGES,
|
||||
})
|
||||
if (generation !== this.generation || this.state !== 'ready' || !result.ok) return
|
||||
const older = result.value.events
|
||||
if (older.length === 0) {
|
||||
this.hasMore = result.value.hasMore
|
||||
return
|
||||
}
|
||||
const tail = older.at(-1)
|
||||
if (tail === undefined || tail.event.seq + 1 !== this.baseSeq) {
|
||||
console.error(
|
||||
`[web-runtime] inspection history page discontinuous: tail seq ${tail?.event.seq} vs baseSeq ${this.baseSeq}`,
|
||||
)
|
||||
this.hasMore = false
|
||||
return
|
||||
}
|
||||
this.entries = [...older, ...this.entries]
|
||||
this.inspectionEntries = compactHistoryInspectionEntries([...this.entries])
|
||||
this.baseSeq = older[0]?.event.seq ?? this.baseSeq
|
||||
this.hasMore = result.value.hasMore
|
||||
} catch (error) {
|
||||
console.error('[web-runtime] inspection history paging failed:', error)
|
||||
}
|
||||
})()
|
||||
const settled = operation.finally(() => {
|
||||
if (this.olderPromise !== settled) return
|
||||
this.olderPromise = null
|
||||
this.publishDirtyNow()
|
||||
})
|
||||
this.olderPromise = settled
|
||||
return settled
|
||||
}
|
||||
|
||||
private installTail(
|
||||
tail: readonly HistoryEntry[],
|
||||
hasMore: boolean,
|
||||
replace: boolean,
|
||||
): void {
|
||||
if (replace) {
|
||||
this.entries = [...tail]
|
||||
this.hasMore = hasMore
|
||||
} else {
|
||||
const firstSeq = tail[0]?.event.seq
|
||||
const prefix = firstSeq === undefined
|
||||
? this.entries
|
||||
: this.entries.filter(entry => entry.event.seq < firstSeq)
|
||||
this.entries = [...prefix, ...tail]
|
||||
}
|
||||
this.baseSeq = this.entries[0]?.event.seq ?? 0
|
||||
this.inspectionEntries = compactHistoryInspectionEntries([...this.entries])
|
||||
const buffered = this.liveBuffer
|
||||
this.liveBuffer = []
|
||||
for (const entry of buffered) this.appendLive(entry)
|
||||
this.publishDirtyNow()
|
||||
}
|
||||
|
||||
private acceptLive(entry: HistoryEntry): void {
|
||||
if (this.state === 'loading' || this.stitching) {
|
||||
this.liveBuffer.push(entry)
|
||||
return
|
||||
}
|
||||
if (this.state !== 'ready') return
|
||||
const tailSeq = this.tailSeq()
|
||||
if (tailSeq !== null && entry.event.seq > tailSeq + 1) {
|
||||
this.liveBuffer.push(entry)
|
||||
void this.repairGap()
|
||||
return
|
||||
}
|
||||
if (
|
||||
entry.event.type === 'assistant/chunk'
|
||||
&& entry.event.data.chunk.type !== 'usage'
|
||||
) {
|
||||
if (!this.appendIncrementalChunk(entry, entry.event)) return
|
||||
this.publishStreamDirty()
|
||||
return
|
||||
}
|
||||
this.appendLive(entry)
|
||||
this.publishDirtyNow()
|
||||
}
|
||||
|
||||
private appendLive(entry: HistoryEntry): void {
|
||||
const tailSeq = this.tailSeq()
|
||||
if (tailSeq !== null && entry.event.seq <= tailSeq) return
|
||||
this.entries.push(entry)
|
||||
this.inspectionEntries = [...this.inspectionEntries, entry]
|
||||
if (entry.event.type === 'assistant/message') {
|
||||
this.inspectionEntries = compactHistoryInspectionEntries(this.inspectionEntries)
|
||||
}
|
||||
}
|
||||
|
||||
/** Append a chunk against the cached finalized projection; false means no visible publish. */
|
||||
private appendIncrementalChunk(
|
||||
entry: HistoryEntry,
|
||||
event: SessionEvent<'assistant/chunk'>,
|
||||
): boolean {
|
||||
const { turn, step, chunk } = event.data
|
||||
if (!isVisibleAssistantChunk(chunk.type)) {
|
||||
const inspection = this.currentInspection()
|
||||
this.appendLive(entry)
|
||||
this.inspectionCache = { entries: this.inspectionEntries, value: inspection }
|
||||
return false
|
||||
}
|
||||
const base = this.currentInspection()
|
||||
if (
|
||||
this.streamPartial === null
|
||||
|| this.streamPartial.turn !== turn
|
||||
|| this.streamPartial.step !== step
|
||||
) {
|
||||
const current = base.partial
|
||||
this.streamPartial = new PartialAccumulator(
|
||||
turn,
|
||||
step,
|
||||
current?.turn === turn && current.step === step ? current.blocks : [],
|
||||
)
|
||||
}
|
||||
this.streamPartial.push(chunk)
|
||||
this.appendLive(entry)
|
||||
this.inspectionCache = {
|
||||
entries: this.inspectionEntries,
|
||||
value: { ...base, partial: this.streamPartial.toPartial() },
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** Coalesce token-stream projection and rendering work to one publish per browser frame. */
|
||||
private publishStreamDirty(): void {
|
||||
if (this.streamPublishToken !== null) return
|
||||
const token = {}
|
||||
this.streamPublishToken = token
|
||||
const publish = () => {
|
||||
if (this.streamPublishToken !== token) return
|
||||
this.streamPublishToken = null
|
||||
this.notifier.markDirty()
|
||||
}
|
||||
if (typeof globalThis.requestAnimationFrame === 'function') {
|
||||
globalThis.requestAnimationFrame(publish)
|
||||
} else {
|
||||
queueMicrotask(publish)
|
||||
}
|
||||
}
|
||||
|
||||
/** Publish structural changes immediately and invalidate an older scheduled stream publish. */
|
||||
private publishDirtyNow(): void {
|
||||
this.streamPublishToken = null
|
||||
this.streamPartial = null
|
||||
this.notifier.markDirty()
|
||||
}
|
||||
|
||||
private async repairGap(): Promise<void> {
|
||||
if (this.stitching) return
|
||||
this.stitching = true
|
||||
const generation = this.generation
|
||||
try {
|
||||
const { result } = await this.api.sessions.history({
|
||||
sessionId: this.sessionId,
|
||||
maxMessages: HISTORY_PAGE_MESSAGES,
|
||||
})
|
||||
if (result.ok && generation === this.generation && this.state === 'ready') {
|
||||
this.installTail(result.value.events, result.value.hasMore, false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[web-runtime] inspection history gap repair failed:', error)
|
||||
} finally {
|
||||
if (generation === this.generation) this.stitching = false
|
||||
}
|
||||
}
|
||||
|
||||
private tailSeq(): number | null {
|
||||
return this.entries.at(-1)?.event.seq ?? null
|
||||
}
|
||||
|
||||
private buildSnapshot(): SessionHistorySnapshot {
|
||||
return {
|
||||
state: this.state,
|
||||
error: this.error,
|
||||
hasMore: this.hasMore,
|
||||
baseSeq: this.baseSeq,
|
||||
inspection: this.currentInspection(),
|
||||
}
|
||||
}
|
||||
|
||||
/** Inspection pinned to the source's current immutable entry array. */
|
||||
private currentInspection(): SessionHistorySnapshot['inspection'] {
|
||||
if (this.inspectionCache?.entries !== this.inspectionEntries) {
|
||||
const entries = this.inspectionEntries
|
||||
this.inspectionCache = {
|
||||
entries,
|
||||
value: createHistoryInspection(() => entries),
|
||||
}
|
||||
}
|
||||
return this.inspectionCache.value
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import type {
|
||||
ConversationContextReader, ConversationEventInput, ConversationLocationData, ConversationMatch,
|
||||
ConversationNodeContext, ConversationNodeDefinition, ConversationPreviousContext,
|
||||
ConversationLocationDataScope, ConversationPublication, ConversationViewBuilder,
|
||||
ConversationViewDefinition, ConversationViewNode,
|
||||
ConversationViewDefinition, ConversationViewNode, ConversationViewSnapshotMap,
|
||||
ConversationViewSnapshotStore,
|
||||
} from '../contract/conversation.ts'
|
||||
import { conversationContextKey } from '../contract/conversation.ts'
|
||||
import {
|
||||
@@ -133,7 +134,7 @@ export interface ConversationViewDefinitions {
|
||||
* Session-owned incremental engine that assembles business Contexts from a
|
||||
* contiguous Event window and materializes registered view snapshots.
|
||||
*/
|
||||
export class ConversationNodeAssembler {
|
||||
export class ConversationNodeAssembler implements ConversationViewSnapshotStore {
|
||||
private readonly contexts = new Map<string, InternalContext>()
|
||||
private readonly contextsByKind = new Map<string, InternalContext[]>()
|
||||
private readonly contextsBySeq = new Map<number, Set<InternalContext>>()
|
||||
@@ -266,11 +267,11 @@ export class ConversationNodeAssembler {
|
||||
const allByTarget = new Map<string, ConversationViewNode[]>()
|
||||
for (const target of this.views.keys()) allByTarget.set(target, [])
|
||||
for (const context of this.contexts.values()) {
|
||||
for (const target of this.views.keys()) {
|
||||
const node = this.buildNode(context, target)
|
||||
context.current.set(target, node)
|
||||
if (node !== null) allByTarget.get(target)?.push(node)
|
||||
}
|
||||
const target = context.definition.target
|
||||
if (target === undefined || !this.views.has(target)) continue
|
||||
const node = this.buildNode(context, target)
|
||||
context.current.set(target, node)
|
||||
if (node !== null) allByTarget.get(target)?.push(node)
|
||||
}
|
||||
for (const view of this.views.values()) {
|
||||
view.snapshot = view.builder.replace({
|
||||
@@ -288,17 +289,17 @@ export class ConversationNodeAssembler {
|
||||
for (const target of this.views.keys()) upsertsByTarget.set(target, [])
|
||||
if (this.applyDirtyLocationData()) this.timelineDirty = true
|
||||
for (const context of this.dirty) {
|
||||
for (const target of this.views.keys()) {
|
||||
const previous = context.current.get(target) ?? null
|
||||
const node = this.buildNode(context, target)
|
||||
if (node === null && previous !== null) {
|
||||
throw new Error(
|
||||
`conversation Definition "${context.kind}" withdrew materialized target "${target}"; return the same key with hidden visibility instead`,
|
||||
)
|
||||
}
|
||||
context.current.set(target, node)
|
||||
if (node !== null) upsertsByTarget.get(target)?.push(node)
|
||||
const target = context.definition.target
|
||||
if (target === undefined || !this.views.has(target)) continue
|
||||
const previous = context.current.get(target) ?? null
|
||||
const node = this.buildNode(context, target)
|
||||
if (node === null && previous !== null) {
|
||||
throw new Error(
|
||||
`conversation Definition "${context.kind}" withdrew materialized target "${target}"; return the same key with hidden visibility instead`,
|
||||
)
|
||||
}
|
||||
context.current.set(target, node)
|
||||
if (node !== null) upsertsByTarget.get(target)?.push(node)
|
||||
}
|
||||
this.dirty.clear()
|
||||
const timelineDirty = this.timelineDirty
|
||||
@@ -323,6 +324,12 @@ export class ConversationNodeAssembler {
|
||||
return this.views.get(target)?.snapshot
|
||||
}
|
||||
|
||||
get<Target extends Extract<keyof ConversationViewSnapshotMap, string>>(
|
||||
target: Target,
|
||||
): ConversationViewSnapshotMap[Target] | undefined {
|
||||
return this.snapshot(target) as ConversationViewSnapshotMap[Target] | undefined
|
||||
}
|
||||
|
||||
private sortedInputs(): ConversationEventInput[] {
|
||||
return [...this.inputs.values()].sort((left, right) => left.event.seq - right.event.seq)
|
||||
}
|
||||
@@ -358,18 +365,19 @@ export class ConversationNodeAssembler {
|
||||
role: ConversationMatch['role'],
|
||||
) => ConversationPublication,
|
||||
): ConversationPublication {
|
||||
let matched = false
|
||||
const matchedTargets = new Set<string>()
|
||||
let publication: ConversationPublication = 'none'
|
||||
for (const definition of this.eventDefinitions.entries()) {
|
||||
const result = definition.match(input.event)
|
||||
if (result === null) continue
|
||||
matched = true
|
||||
if (definition.target !== undefined) matchedTargets.add(definition.target)
|
||||
publication = maximumPublication(publication, accept(definition, result.id, result.role))
|
||||
}
|
||||
if (!matched) {
|
||||
const fallback = this.eventDefinitions.fallbackEntry()
|
||||
const result = fallback?.match(input.event) ?? null
|
||||
if (fallback !== undefined && result !== null) {
|
||||
const fallback = this.eventDefinitions.fallbackEntry()
|
||||
const target = fallback?.target
|
||||
if (fallback !== undefined && target !== undefined && !matchedTargets.has(target)) {
|
||||
const result = fallback.match(input.event)
|
||||
if (result !== null) {
|
||||
publication = maximumPublication(publication, accept(fallback, result.id, result.role))
|
||||
}
|
||||
}
|
||||
@@ -697,7 +705,8 @@ export class ConversationNodeAssembler {
|
||||
}
|
||||
|
||||
private buildNode(context: InternalContext, target: string): ConversationViewNode | null {
|
||||
const node = context.definition.buildViewNode(contextSnapshot(context), target)
|
||||
if (context.definition.target !== target || context.definition.buildViewNode === undefined) return null
|
||||
const node = context.definition.buildViewNode(contextSnapshot(context))
|
||||
if (node === null) return null
|
||||
if (node.key !== context.key) {
|
||||
throw new Error(`conversation Definition "${context.kind}" returned unstable key "${node.key}"; expected "${context.key}"`)
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
import type { PendingInteraction } from './pending.ts'
|
||||
import type { ContextProvenanceView, KnownContextForm } from './context-provenance.ts'
|
||||
import type {
|
||||
ChatConversationViewNode, ConversationTimelineSnapshot,
|
||||
ChatConversationViewNode, ConversationTimelineSnapshot, ConversationViewSnapshotStore,
|
||||
} from '../contract/conversation.ts'
|
||||
export type { TodoItem }
|
||||
|
||||
@@ -384,6 +384,11 @@ export interface ChatSnapshot {
|
||||
const EMPTY_LIST: readonly never[] = []
|
||||
const EMPTY_TIMELINE: ConversationTimelineSnapshot = { turnOrder: EMPTY_LIST, turns: new Map() }
|
||||
|
||||
/** Empty target store used by fixtures and Sessions without registered views. */
|
||||
export const EMPTY_CONVERSATION_VIEWS: ConversationViewSnapshotStore = {
|
||||
get: () => undefined,
|
||||
}
|
||||
|
||||
/** Empty Chat target used before a view builder is registered. */
|
||||
export const EMPTY_CHAT_SNAPSHOT: ChatSnapshot = {
|
||||
order: EMPTY_LIST,
|
||||
@@ -408,6 +413,8 @@ export const EMPTY_CHAT_SNAPSHOT: ChatSnapshot = {
|
||||
/** The immutable snapshot contract Session hands to uSES (see the web client architecture RFC). */
|
||||
export interface ConversationSnapshot {
|
||||
sessionId: SessionId
|
||||
/** Registered target snapshots assembled from Session events. */
|
||||
views: ConversationViewSnapshotStore
|
||||
/** Final Chat target assembled from independently registered business Definitions. */
|
||||
chat: ChatSnapshot
|
||||
/** Legacy top-level compatibility field mirrored from the registered Chat Definitions. */
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import type { ToolSchema } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { HistoryEntry } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type {
|
||||
ConversationNode, PartialAssistant, RunningToolCall,
|
||||
} from './conversation.ts'
|
||||
import type { ConversationContext } from './conversation-context.ts'
|
||||
import { projectConversationHistory } from '../session-history/history-fold.ts'
|
||||
import { inspectRequests, type RequestView } from './request-inspection.ts'
|
||||
|
||||
function assistantStepKey(turn: number, step: number): string {
|
||||
return `${turn}\u0000${step}`
|
||||
}
|
||||
|
||||
function isFirstTokenCandidate(entry: HistoryEntry): boolean {
|
||||
const event = entry.event
|
||||
if (event.type !== 'assistant/chunk') return false
|
||||
switch (event.data.chunk.type) {
|
||||
case 'text-delta':
|
||||
case 'reasoning-delta':
|
||||
return event.data.chunk.text !== ''
|
||||
case 'tool-call-delta':
|
||||
return event.data.chunk.argumentsDelta !== '' || event.data.chunk.name !== undefined
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Lazily derived inspection data for one immutable session-history window. */
|
||||
export interface SessionHistoryInspection {
|
||||
eventNodes: readonly ConversationNode[]
|
||||
contexts: readonly ConversationContext[]
|
||||
requests: readonly RequestView[]
|
||||
callSchemas: ReadonlyMap<string, ToolSchema>
|
||||
interruptedNodes: readonly ConversationNode[]
|
||||
partial: PartialAssistant | null
|
||||
runningCalls: readonly RunningToolCall[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove completed-step token payloads that no inspection projection reads.
|
||||
* The first visible token preserves timing, usage chunks preserve accounting,
|
||||
* and unfinished steps retain every chunk for live or interrupted content.
|
||||
* @param entries - Contiguous raw history entries in sequence order.
|
||||
* @returns A projection-equivalent, usually much smaller entry ledger.
|
||||
*/
|
||||
export function compactHistoryInspectionEntries(
|
||||
entries: readonly HistoryEntry[],
|
||||
): readonly HistoryEntry[] {
|
||||
const completedSteps = new Set<string>()
|
||||
for (const { event } of entries) {
|
||||
if (event.type === 'assistant/message') {
|
||||
completedSteps.add(assistantStepKey(event.data.turn, event.data.step))
|
||||
}
|
||||
}
|
||||
|
||||
const firstTokenSteps = new Set<string>()
|
||||
const compacted: HistoryEntry[] = []
|
||||
let changed = false
|
||||
for (const entry of entries) {
|
||||
const event = entry.event
|
||||
if (event.type !== 'assistant/chunk') {
|
||||
compacted.push(entry)
|
||||
continue
|
||||
}
|
||||
const key = assistantStepKey(event.data.turn, event.data.step)
|
||||
if (!completedSteps.has(key) || event.data.chunk.type === 'usage') {
|
||||
compacted.push(entry)
|
||||
continue
|
||||
}
|
||||
if (isFirstTokenCandidate(entry) && !firstTokenSteps.has(key)) {
|
||||
firstTokenSteps.add(key)
|
||||
compacted.push(entry)
|
||||
} else {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed ? compacted : entries
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a lazy inspection projection over an immutable history window.
|
||||
* Conversation consumers retain the cheap wrapper; only Trajectory snapshots
|
||||
* the entries and replays event order and request lifecycle state.
|
||||
* @param loadEntries - Lazily snapshots contiguous raw entries in sequence order.
|
||||
* @returns Lazy, memoized inspection fields for that exact window.
|
||||
*/
|
||||
export function createHistoryInspection(
|
||||
loadEntries: () => readonly HistoryEntry[],
|
||||
): SessionHistoryInspection {
|
||||
let entries: readonly HistoryEntry[] | undefined
|
||||
let conversation: ReturnType<typeof projectConversationHistory> | undefined
|
||||
let requests: ReturnType<typeof inspectRequests> | undefined
|
||||
const historyEntries = () => entries ??= loadEntries()
|
||||
const conversationProjection = () =>
|
||||
conversation ??= projectConversationHistory(historyEntries())
|
||||
const requestProjection = () =>
|
||||
requests ??= inspectRequests(historyEntries())
|
||||
return {
|
||||
get eventNodes() {
|
||||
return conversationProjection().eventNodes
|
||||
},
|
||||
get contexts() {
|
||||
return conversationProjection().contexts
|
||||
},
|
||||
get interruptedNodes() {
|
||||
return conversationProjection().interruptedNodes
|
||||
},
|
||||
get partial() {
|
||||
return conversationProjection().partial
|
||||
},
|
||||
get runningCalls() {
|
||||
return conversationProjection().runningCalls
|
||||
},
|
||||
get requests() {
|
||||
return requestProjection().requests
|
||||
},
|
||||
get callSchemas() {
|
||||
return requestProjection().callSchemas
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,7 @@
|
||||
// Request-centric inspection read model. Ordinary generation and compaction
|
||||
// calls share one chronological projection; presentation-specific grouping
|
||||
// remains in the trajectory consumer.
|
||||
|
||||
import type { ContentBlock, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { HistoryEntry } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import type {} from '@deepseek-ai/dsh-compact/types'
|
||||
import type {} from '@deepseek-ai/dsh-llm-retry/types'
|
||||
import type {} from '@deepseek-ai/dsh-tools/types'
|
||||
import type { ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm/types'
|
||||
import type {
|
||||
AssistantProvenanceView, AssistantRequestConfig,
|
||||
} from './conversation.ts'
|
||||
import { displayFailureMessage } from './failure-display.ts'
|
||||
|
||||
export type {
|
||||
AssistantProvenanceView, AssistantRequestConfig,
|
||||
@@ -54,7 +44,7 @@ interface RequestViewBase {
|
||||
resultSeq?: number
|
||||
}
|
||||
|
||||
/** One ordinary assistant generation reconstructed from durable request events. */
|
||||
/** One ordinary assistant generation assembled from durable request events. */
|
||||
interface AssistantRequestView extends RequestViewBase {
|
||||
purpose: 'assistant'
|
||||
turn: number
|
||||
@@ -85,321 +75,11 @@ interface CompactionRequestView extends RequestViewBase {
|
||||
rawOutput?: readonly ContentBlock[]
|
||||
}
|
||||
|
||||
/** One provider request reconstructed from durable request lifecycle events. */
|
||||
/** One provider request assembled from durable request lifecycle events. */
|
||||
export type RequestView = AssistantRequestView | CompactionRequestView
|
||||
|
||||
/** Immutable request-centric projection derived from one history window. */
|
||||
/** Request data consumed by the stage-oriented Trajectory layout. */
|
||||
export interface RequestInspectionSnapshot {
|
||||
requests: readonly RequestView[]
|
||||
callSchemas: ReadonlyMap<string, ToolSchema>
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the request-centric read model from one immutable history window.
|
||||
* Compaction participates as a request purpose rather than a parallel
|
||||
* top-level collection. A leading resume/change header exposes its prompt but
|
||||
* cannot project a change until the preceding header enters the window.
|
||||
* @param entries - Contiguous raw session history.
|
||||
* @returns Requests and call-time schemas derived from that history.
|
||||
*/
|
||||
export function inspectRequests(
|
||||
entries: readonly HistoryEntry[],
|
||||
): RequestInspectionSnapshot {
|
||||
const events = entries.map(entry => entry.event)
|
||||
return {
|
||||
requests: deriveRequests(events),
|
||||
callSchemas: deriveCallSchemas(events),
|
||||
}
|
||||
}
|
||||
|
||||
function requestKey(turn: number, step: number): string {
|
||||
return `${turn}\u0000${step}`
|
||||
}
|
||||
|
||||
function addTokenUsage(current: unknown, next: TokenUsage): TokenUsage {
|
||||
const previous = current as TokenUsage | undefined
|
||||
return {
|
||||
inputTokens: (previous?.inputTokens ?? 0) + next.inputTokens,
|
||||
outputTokens: (previous?.outputTokens ?? 0) + next.outputTokens,
|
||||
...(previous?.cacheReadTokens === undefined && next.cacheReadTokens === undefined
|
||||
? {}
|
||||
: {
|
||||
cacheReadTokens:
|
||||
(previous?.cacheReadTokens ?? 0) + (next.cacheReadTokens ?? 0),
|
||||
}),
|
||||
...(previous?.cacheWriteTokens === undefined && next.cacheWriteTokens === undefined
|
||||
? {}
|
||||
: {
|
||||
cacheWriteTokens:
|
||||
(previous?.cacheWriteTokens ?? 0) + (next.cacheWriteTokens ?? 0),
|
||||
}),
|
||||
...(previous?.reasoningTokens === undefined && next.reasoningTokens === undefined
|
||||
? {}
|
||||
: {
|
||||
reasoningTokens:
|
||||
(previous?.reasoningTokens ?? 0) + (next.reasoningTokens ?? 0),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function deriveCallSchemas(
|
||||
events: readonly SessionEvent[],
|
||||
): ReadonlyMap<string, ToolSchema> {
|
||||
let active = new Map<string, ToolSchema>()
|
||||
const calls = new Map<string, ToolSchema>()
|
||||
const capture = (callId: string, name: string): void => {
|
||||
if (calls.has(callId)) return
|
||||
const schema = active.get(name)
|
||||
if (schema !== undefined) calls.set(callId, schema)
|
||||
}
|
||||
for (const event of events) {
|
||||
if (event.type === 'request/header') {
|
||||
const tools: unknown = event.data.header.tools
|
||||
active = new Map(
|
||||
Array.isArray(tools)
|
||||
? (tools as ToolSchema[]).map(schema => [schema.name, schema])
|
||||
: [],
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (event.type === 'tool/call') {
|
||||
capture(String(event.data.callId), event.data.name)
|
||||
continue
|
||||
}
|
||||
if (event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch') {
|
||||
capture(String(event.data.subCallId), event.data.name)
|
||||
}
|
||||
}
|
||||
return calls
|
||||
}
|
||||
|
||||
function promptChange(
|
||||
previous: ConversationPromptSnapshot | undefined,
|
||||
prompt: ConversationPromptSnapshot,
|
||||
event: SessionEvent<'request/header'>,
|
||||
): RequestPromptChange | undefined {
|
||||
if (previous === undefined && event.data.reason !== 'initial') return
|
||||
const systemChanged = previous !== undefined && previous.system !== prompt.system
|
||||
const toolsChanged = previous !== undefined
|
||||
&& JSON.stringify(previous.tools) !== JSON.stringify(prompt.tools)
|
||||
if (previous !== undefined && !systemChanged && !toolsChanged) return
|
||||
return {
|
||||
seq: event.seq,
|
||||
time: event.time,
|
||||
kind: previous === undefined
|
||||
? 'initial'
|
||||
: systemChanged && toolsChanged
|
||||
? 'system-and-tools'
|
||||
: systemChanged
|
||||
? 'system'
|
||||
: 'tools',
|
||||
...(previous === undefined ? {} : { previous }),
|
||||
}
|
||||
}
|
||||
|
||||
/** Project ordinary and compaction provider calls into one chronological request stream. */
|
||||
function deriveRequests(events: readonly SessionEvent[]): readonly RequestView[] {
|
||||
const requests: RequestView[] = []
|
||||
const ordinaryByStep = new Map<string, number>()
|
||||
const lastStepByTurn = new Map<number, string>()
|
||||
let activeStep: string | undefined
|
||||
let activePrompt: ConversationPromptSnapshot | undefined
|
||||
let activeCompaction: number | undefined
|
||||
|
||||
const updateAssistant = (
|
||||
index: number | undefined,
|
||||
change: Partial<Omit<AssistantRequestView, 'purpose'>>,
|
||||
): void => {
|
||||
if (index === undefined) return
|
||||
const request = requests[index]
|
||||
if (request?.purpose === 'assistant') requests[index] = { ...request, ...change }
|
||||
}
|
||||
const updateCompaction = (
|
||||
index: number | undefined,
|
||||
change: Partial<Omit<CompactionRequestView, 'purpose'>>,
|
||||
): void => {
|
||||
if (index === undefined) return
|
||||
const request = requests[index]
|
||||
if (request?.purpose === 'compaction') requests[index] = { ...request, ...change }
|
||||
}
|
||||
|
||||
for (const sourceEvent of events) {
|
||||
if (sourceEvent.type === 'step/start') {
|
||||
const { turn, step } = sourceEvent.data
|
||||
const key = requestKey(turn, step)
|
||||
ordinaryByStep.set(key, requests.length)
|
||||
lastStepByTurn.set(turn, key)
|
||||
requests.push({
|
||||
purpose: 'assistant',
|
||||
startSeq: sourceEvent.seq,
|
||||
turn,
|
||||
step,
|
||||
startedAt: sourceEvent.time,
|
||||
completedAt: null,
|
||||
status: 'running',
|
||||
...(activePrompt === undefined
|
||||
? {}
|
||||
: { prompt: activePrompt, requestConfig: activePrompt.config }),
|
||||
})
|
||||
activeStep = key
|
||||
continue
|
||||
}
|
||||
if (sourceEvent.type === 'request/header') {
|
||||
const tools: unknown = sourceEvent.data.header.tools
|
||||
const prompt: ConversationPromptSnapshot = {
|
||||
config: sourceEvent.data.header.config,
|
||||
system: sourceEvent.data.header.system ?? '',
|
||||
tools: Array.isArray(tools) ? tools as ToolSchema[] : [],
|
||||
}
|
||||
const change = promptChange(activePrompt, prompt, sourceEvent)
|
||||
activePrompt = prompt
|
||||
updateAssistant(activeStep === undefined ? undefined : ordinaryByStep.get(activeStep), {
|
||||
prompt,
|
||||
requestConfig: prompt.config,
|
||||
...(change === undefined ? {} : { promptChange: change }),
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (
|
||||
sourceEvent.type === 'assistant/chunk'
|
||||
&& sourceEvent.data.chunk.type === 'usage'
|
||||
) {
|
||||
const index = ordinaryByStep.get(
|
||||
requestKey(sourceEvent.data.turn, sourceEvent.data.step),
|
||||
)
|
||||
const request = index === undefined ? undefined : requests[index]
|
||||
updateAssistant(index, {
|
||||
usage: addTokenUsage(
|
||||
request?.purpose === 'assistant' ? request.usage : undefined,
|
||||
sourceEvent.data.chunk.usage,
|
||||
),
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (sourceEvent.type === 'assistant/message') {
|
||||
const index = ordinaryByStep.get(
|
||||
requestKey(sourceEvent.data.turn, sourceEvent.data.step),
|
||||
)
|
||||
const request = index === undefined ? undefined : requests[index]
|
||||
updateAssistant(index, {
|
||||
completedAt: sourceEvent.time,
|
||||
status: 'complete',
|
||||
resultSeq: sourceEvent.seq,
|
||||
provenance: {
|
||||
provider: sourceEvent.data.message.source.provider,
|
||||
model: sourceEvent.data.message.source.model,
|
||||
},
|
||||
...(request?.purpose === 'assistant'
|
||||
&& request.usage !== undefined
|
||||
|| sourceEvent.data.usage === undefined
|
||||
? {}
|
||||
: { usage: sourceEvent.data.usage }),
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (sourceEvent.type === 'step/end') {
|
||||
const key = requestKey(sourceEvent.data.turn, sourceEvent.data.step)
|
||||
const index = ordinaryByStep.get(key)
|
||||
const request = index === undefined ? undefined : requests[index]
|
||||
if (request?.purpose === 'assistant' && request.status === 'running') {
|
||||
updateAssistant(index, {
|
||||
completedAt: sourceEvent.time,
|
||||
status: 'error',
|
||||
})
|
||||
}
|
||||
if (activeStep === key) activeStep = undefined
|
||||
continue
|
||||
}
|
||||
if (sourceEvent.type === 'llm/retry') {
|
||||
const data = sourceEvent.data
|
||||
updateAssistant(ordinaryByStep.get(requestKey(data.turn, data.step)), {
|
||||
status: 'error',
|
||||
error: displayFailureMessage(data.failure),
|
||||
retry: data.retry,
|
||||
...data.mode === 'normal' ? { maxRetries: data.maxRetries } : {},
|
||||
retryDelayMs: data.delayMs,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (sourceEvent.type === 'turn/end') {
|
||||
const lastStep = lastStepByTurn.get(sourceEvent.data.turn)
|
||||
if (sourceEvent.data.reason.kind === 'error') {
|
||||
updateAssistant(lastStep === undefined ? undefined : ordinaryByStep.get(lastStep), {
|
||||
status: 'error',
|
||||
error: displayFailureMessage(sourceEvent.data.reason.error),
|
||||
})
|
||||
}
|
||||
lastStepByTurn.delete(sourceEvent.data.turn)
|
||||
continue
|
||||
}
|
||||
|
||||
if (sourceEvent.type === 'session/end-seed' && activeCompaction !== undefined) {
|
||||
updateCompaction(activeCompaction, {
|
||||
completedAt: sourceEvent.time,
|
||||
status: 'error',
|
||||
error: 'Compaction was interrupted before completion.',
|
||||
})
|
||||
activeCompaction = undefined
|
||||
continue
|
||||
}
|
||||
if (sourceEvent.type === 'compact/start') {
|
||||
activeCompaction = requests.length
|
||||
requests.push({
|
||||
purpose: 'compaction',
|
||||
startSeq: sourceEvent.seq,
|
||||
turn: sourceEvent.data.turn,
|
||||
step: 0,
|
||||
startedAt: sourceEvent.time,
|
||||
completedAt: null,
|
||||
status: 'running',
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (sourceEvent.type === 'compact/summary' && activeCompaction !== undefined) {
|
||||
const data = sourceEvent.data
|
||||
updateCompaction(activeCompaction, {
|
||||
resultSeq: sourceEvent.seq,
|
||||
summary: data.summary,
|
||||
...(data.rawOutput === undefined ? {} : { rawOutput: data.rawOutput }),
|
||||
provenance: {
|
||||
provider: data.provider,
|
||||
model: data.model,
|
||||
},
|
||||
requestConfig: {
|
||||
provider: data.provider,
|
||||
model: data.model,
|
||||
purpose: 'compaction',
|
||||
...(data.maxTokens === undefined ? {} : { maxTokens: data.maxTokens }),
|
||||
},
|
||||
...(data.usage === undefined ? {} : { usage: data.usage }),
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (
|
||||
sourceEvent.type === 'user/message'
|
||||
&& activeCompaction !== undefined
|
||||
&& isCompactionSource(sourceEvent.data.source)
|
||||
) {
|
||||
updateCompaction(activeCompaction, { replacementSeq: sourceEvent.seq })
|
||||
continue
|
||||
}
|
||||
if (sourceEvent.type !== 'compact/end' || activeCompaction === undefined) continue
|
||||
updateCompaction(activeCompaction, {
|
||||
completedAt: sourceEvent.time,
|
||||
status: sourceEvent.data.error === undefined ? 'complete' : 'error',
|
||||
...(sourceEvent.data.error === undefined ? {} : { error: sourceEvent.data.error }),
|
||||
})
|
||||
activeCompaction = undefined
|
||||
}
|
||||
|
||||
return requests.sort((left, right) => left.startSeq - right.startSeq)
|
||||
}
|
||||
|
||||
function isCompactionSource(source: unknown): boolean {
|
||||
return typeof source === 'object'
|
||||
&& source !== null
|
||||
&& 'kind' in source
|
||||
&& source.kind === 'plugin'
|
||||
&& 'plugin' in source
|
||||
&& source.plugin === 'compact'
|
||||
}
|
||||
|
||||
@@ -727,6 +727,7 @@ export class Session implements SessionFace {
|
||||
const legacy = chat.legacy
|
||||
return {
|
||||
sessionId: this.sessionId,
|
||||
views: this.conversation,
|
||||
chat,
|
||||
nodes: legacy.nodes,
|
||||
turnTimings: legacy.turnTimings,
|
||||
|
||||
@@ -126,6 +126,7 @@ describe('runtime client apply', () => {
|
||||
const rebuild = vi.spyOn(Session.prototype, 'rebuildConversationRegistry')
|
||||
const definition: ConversationNodeDefinition<null> = {
|
||||
kind: 'registry-probe',
|
||||
target: 'chat',
|
||||
match: () => null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
|
||||
@@ -30,10 +30,16 @@ interface TestSnapshot {
|
||||
}
|
||||
|
||||
class TestEventDefinitions {
|
||||
readonly definitions: readonly ConversationNodeDefinition[]
|
||||
readonly fallback: ConversationNodeDefinition | undefined
|
||||
|
||||
constructor(
|
||||
readonly definitions: readonly ConversationNodeDefinition[],
|
||||
readonly fallback?: ConversationNodeDefinition,
|
||||
) {}
|
||||
definitions: readonly ConversationNodeDefinition[],
|
||||
fallback?: ConversationNodeDefinition,
|
||||
) {
|
||||
this.definitions = definitions
|
||||
this.fallback = fallback
|
||||
}
|
||||
|
||||
entries(): readonly ConversationNodeDefinition[] {
|
||||
return this.definitions
|
||||
@@ -93,7 +99,10 @@ function chatSnapshot(assembler: ConversationNodeAssembler): TestSnapshot | unde
|
||||
return assembler.snapshot('chat') as TestSnapshot | undefined
|
||||
}
|
||||
|
||||
function node(context: Parameters<ConversationNodeDefinition['buildViewNode']>[0], data: unknown): ConversationViewNode {
|
||||
function node(
|
||||
context: Parameters<NonNullable<ConversationNodeDefinition['buildViewNode']>>[0],
|
||||
data: unknown,
|
||||
): ConversationViewNode {
|
||||
return {
|
||||
key: context.key,
|
||||
kind: context.kind,
|
||||
@@ -103,6 +112,17 @@ function node(context: Parameters<ConversationNodeDefinition['buildViewNode']>[0
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackDefinition(start: () => string): ConversationNodeDefinition<string> {
|
||||
return {
|
||||
kind: 'fallback',
|
||||
target: 'chat',
|
||||
match: event => ({ id: String(event.seq), role: 'start' }),
|
||||
start,
|
||||
update: context => context.state,
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
}
|
||||
|
||||
describe('ConversationNodeAssembler', () => {
|
||||
it('appends through an exact business-id Context without replaying unrelated Contexts', () => {
|
||||
const starts = vi.fn((
|
||||
@@ -122,6 +142,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
},
|
||||
start: starts,
|
||||
update: updates,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -173,6 +194,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
matchCollections.add(context.matches)
|
||||
return updates(context)
|
||||
},
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -208,6 +230,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
},
|
||||
start: starts,
|
||||
update: updates,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -247,6 +270,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
},
|
||||
start: () => ({ settled: false }),
|
||||
update: updates,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state ?? { pendingStart: true }),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -280,6 +304,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: event.type === 'turn/start' ? { id: 'one', role: 'update' } : null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -301,6 +326,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: (_context, match) => Number((match.event.data as { value?: unknown }).value ?? 0),
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
const consumerStart = vi.fn((
|
||||
@@ -315,6 +341,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: consumerStart,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -344,6 +371,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: (_context, match) => match.event.seq,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
const consumer: ConversationNodeDefinition<number> = {
|
||||
@@ -353,6 +381,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: (_context, _match, reader) => reader.previous<number>('source')?.state ?? -1,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -397,6 +426,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: consumerStart,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -425,6 +455,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
},
|
||||
start: () => 1,
|
||||
update: (_context, match) => (match.event.data as unknown as { value: number }).value,
|
||||
target: 'chat',
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
const consumerStart = vi.fn((
|
||||
@@ -439,6 +470,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: consumerStart,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -468,6 +500,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
},
|
||||
start: () => 1,
|
||||
update: (_context, match) => (match.event.data as unknown as { value: number }).value,
|
||||
target: 'chat',
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
const sourceX: ConversationNodeDefinition<number> = {
|
||||
@@ -479,6 +512,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
},
|
||||
start: () => 10,
|
||||
update: (_context, match) => (match.event.data as unknown as { value: number }).value,
|
||||
target: 'chat',
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
const middle: ConversationNodeDefinition<number> = {
|
||||
@@ -491,6 +525,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
+ (reader.previous<number>('diamond-x')?.state ?? 0)
|
||||
),
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const consumer: ConversationNodeDefinition<number> = {
|
||||
@@ -503,6 +538,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
+ (reader.previous<number>('diamond-b')?.state ?? 0)
|
||||
),
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -538,6 +574,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: starts,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -609,6 +646,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
value: { valueSeenFromStep: stepValue ?? -1 },
|
||||
}
|
||||
},
|
||||
target: 'chat',
|
||||
buildViewNode: (context) => {
|
||||
const location = context.start?.location
|
||||
if (location?.kind !== 'step') return null
|
||||
@@ -646,6 +684,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.start?.location.kind === 'turn'
|
||||
? context.start.location.turn.steps.length
|
||||
: -1),
|
||||
@@ -701,6 +740,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
},
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: (context) => {
|
||||
const location = context.start?.location
|
||||
const data = location?.kind === 'step'
|
||||
@@ -734,6 +774,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.start?.location.kind),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -760,6 +801,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: (context) => {
|
||||
const location = context.start?.location
|
||||
return node(context, location?.kind === 'step'
|
||||
@@ -792,6 +834,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: (context) => {
|
||||
const location = context.start?.location
|
||||
return node(context, location?.kind === 'step'
|
||||
@@ -826,6 +869,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
: null,
|
||||
start: seen,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -841,24 +885,64 @@ describe('ConversationNodeAssembler', () => {
|
||||
expect(seen).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('does not invoke the fallback when an ordinary non-rendering Definition claims an event', () => {
|
||||
it('invokes the fallback when only a State-only Definition claims an event', () => {
|
||||
const fallbackStart = vi.fn(() => 'fallback')
|
||||
const claimed: ConversationNodeDefinition<null> = {
|
||||
kind: 'claimed-state',
|
||||
match: event => (event.type as string) === 'command/run'
|
||||
? { id: 'claimed', role: 'start' }
|
||||
: null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
new TestEventDefinitions([claimed], fallbackDefinition(fallbackStart)),
|
||||
new TestViewDefinitions([testView()]),
|
||||
)
|
||||
|
||||
assembler.replaceWindow([input(at(1, 'command/run', { commandId: 'one', name: 'x' }))], false)
|
||||
assembler.flush()
|
||||
|
||||
expect(fallbackStart).toHaveBeenCalledOnce()
|
||||
expect(chatSnapshot(assembler)?.order).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('invokes the fallback when only another target claims an event', () => {
|
||||
const fallbackStart = vi.fn(() => 'fallback')
|
||||
const claimed: ConversationNodeDefinition<null> = {
|
||||
kind: 'claimed-trajectory',
|
||||
target: 'trajectory',
|
||||
match: event => (event.type as string) === 'command/run'
|
||||
? { id: 'claimed', role: 'start' }
|
||||
: null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
new TestEventDefinitions([claimed], fallbackDefinition(fallbackStart)),
|
||||
new TestViewDefinitions([testView()]),
|
||||
)
|
||||
|
||||
assembler.replaceWindow([input(at(1, 'command/run', { commandId: 'one', name: 'x' }))], false)
|
||||
assembler.flush()
|
||||
|
||||
expect(fallbackStart).toHaveBeenCalledOnce()
|
||||
expect(chatSnapshot(assembler)?.order).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('suppresses the fallback when the same target claims an event', () => {
|
||||
const fallbackStart = vi.fn(() => 'fallback')
|
||||
const claimed: ConversationNodeDefinition<null> = {
|
||||
kind: 'claimed',
|
||||
target: 'chat',
|
||||
match: event => (event.type as string) === 'command/run' ? { id: 'claimed', role: 'start' } : null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
const fallback: ConversationNodeDefinition<string> = {
|
||||
kind: 'fallback',
|
||||
match: event => ({ id: String(event.seq), role: 'start' }),
|
||||
start: fallbackStart,
|
||||
update: context => context.state,
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
new TestEventDefinitions([claimed], fallback),
|
||||
new TestEventDefinitions([claimed], fallbackDefinition(fallbackStart)),
|
||||
new TestViewDefinitions([testView()]),
|
||||
)
|
||||
assembler.replaceWindow([input(at(1, 'command/run', { commandId: 'one', name: 'x' }))], false)
|
||||
@@ -878,6 +962,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
},
|
||||
start: () => true,
|
||||
update: () => false,
|
||||
target: 'chat',
|
||||
buildViewNode: context => context.state === true ? node(context, true) : null,
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
@@ -900,6 +985,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
match: event => (event.type as string) === 'command/run' ? { id: 'one', role: 'start' } : null,
|
||||
start: () => undefined,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
const startAssembler = new ConversationNodeAssembler(
|
||||
@@ -919,6 +1005,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
},
|
||||
start: () => true,
|
||||
update: () => undefined as never,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const updateAssembler = new ConversationNodeAssembler(
|
||||
@@ -939,6 +1026,7 @@ describe('ConversationNodeAssembler', () => {
|
||||
match: event => (event.type as string) === 'command/run' ? { id: 'one', role: 'start' } : null,
|
||||
start: (_context, match) => match.event.seq,
|
||||
update: context => context.state,
|
||||
target: 'chat',
|
||||
buildViewNode: context => node(context, context.state),
|
||||
}
|
||||
const assembler = new ConversationNodeAssembler(
|
||||
|
||||
@@ -13,6 +13,7 @@ import { FakeApiClient, ok } from './fake-api.ts'
|
||||
function eventDefinition(kind: string): ConversationNodeDefinition<null> {
|
||||
return {
|
||||
kind,
|
||||
target: 'chat',
|
||||
match: () => null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
@@ -71,6 +72,40 @@ describe('Conversation registries', () => {
|
||||
expect(events.fallbackEntry()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects rendering Definitions that omit either target or builder', async () => {
|
||||
const { events } = await bootRegistries()
|
||||
const targetOnly: ConversationNodeDefinition<null> = {
|
||||
kind: 'target-only',
|
||||
target: 'chat',
|
||||
match: () => null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
}
|
||||
const builderOnly: ConversationNodeDefinition<null> = {
|
||||
kind: 'builder-only',
|
||||
match: () => null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
|
||||
expect(() => events.register(targetOnly)).toThrow(/target and buildViewNode together/)
|
||||
expect(() => events.register(builderOnly)).toThrow(/target and buildViewNode together/)
|
||||
})
|
||||
|
||||
it('rejects a State-only Definition as the unmatched-event fallback', async () => {
|
||||
const { events } = await bootRegistries()
|
||||
const fallback: ConversationNodeDefinition<null> = {
|
||||
kind: 'state-only-fallback',
|
||||
match: () => null,
|
||||
start: () => null,
|
||||
update: context => context.state,
|
||||
}
|
||||
|
||||
expect(() => events.registerFallback(fallback))
|
||||
.toThrow('conversation fallback Definition must declare a target')
|
||||
})
|
||||
|
||||
it('rejects duplicate view targets and disposes a view registration once', async () => {
|
||||
const { views } = await bootRegistries()
|
||||
const definition = viewDefinition('chat')
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { projectConversationHistory } from '../src/client/session-history/history-fold.ts'
|
||||
import { compactHistoryInspectionEntries } from '../src/client/sessions/history.ts'
|
||||
import { inspectRequests } from '../src/client/sessions/request-inspection.ts'
|
||||
import { ev } from './event-script.ts'
|
||||
|
||||
const at = (seq: number, event: Record<string, unknown>): SessionEvent =>
|
||||
({ seq, time: 1_700_000_000_000 + seq, ...event }) as unknown as SessionEvent
|
||||
|
||||
describe('projectConversationHistory', () => {
|
||||
it('names an injected context node from its durable source, like the live adapter', () => {
|
||||
// The fold declares its own node mapping (jscpd:ignore in the source), so
|
||||
// the source projection is pinned on both sides independently.
|
||||
const injected = at(0, {
|
||||
type: 'user/message',
|
||||
surfaceOp: 'append',
|
||||
data: createUserMessage({
|
||||
content: [{ type: 'text', text: '<available_skills>…</available_skills>' }],
|
||||
// A plugin source, because the client program does not see the host
|
||||
// packages that merge richer source kinds; those arms are pinned in
|
||||
// context-provenance.spec.ts.
|
||||
source: { kind: 'plugin', plugin: 'dsh-tool-skill', form: 'catalog' },
|
||||
}),
|
||||
})
|
||||
const { contexts } = projectConversationHistory([{ event: injected }])
|
||||
expect(contexts[contexts.length - 1]?.nodes).toMatchObject([{
|
||||
kind: 'context',
|
||||
seq: 0,
|
||||
provenance: { role: 'inject', label: 'dsh-tool-skill' },
|
||||
form: 'catalog',
|
||||
}])
|
||||
})
|
||||
|
||||
it('projects next-step human input as durable steering', () => {
|
||||
const steering = createUserMessage({
|
||||
content: [{ type: 'text', text: 'change course' }],
|
||||
source: { kind: 'user' },
|
||||
})
|
||||
const events = [
|
||||
at(0, { type: 'agent/inbox/spliced', data: {
|
||||
target: 'next-step', start: 0, inserted: [steering],
|
||||
} }),
|
||||
at(1, { type: 'agent/inbox/spliced', data: {
|
||||
target: 'next-step', start: 0, removedCount: 1, inserted: [],
|
||||
} }),
|
||||
at(2, { type: 'user/message', surfaceOp: 'append', data: steering }),
|
||||
]
|
||||
const projection = projectConversationHistory(events.map(event => ({ event })))
|
||||
expect(projection.eventNodes).toMatchObject([{
|
||||
kind: 'steering', messageId: steering.id, seq: 2,
|
||||
}])
|
||||
})
|
||||
|
||||
it('projects a high-sequence history window without synthesizing its unloaded prefix', () => {
|
||||
const baseSeq = 400_000
|
||||
const events = [
|
||||
ev.user(baseSeq, 'loaded tail'),
|
||||
at(baseSeq + 1, {
|
||||
type: 'assistant/message',
|
||||
surfaceOp: { op: 'replace', start: baseSeq, end: baseSeq },
|
||||
sourceEventSeqs: [baseSeq],
|
||||
data: {
|
||||
turn: 80,
|
||||
step: 1,
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'tail summary' }],
|
||||
source: { kind: 'model', provider: 'fake', model: 'fake' },
|
||||
}),
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
const projection = projectConversationHistory(events.map(event => ({ event })))
|
||||
expect(projection.eventNodes.map(node => node.seq)).toEqual([baseSeq, baseSeq + 1])
|
||||
expect(projection.contexts.map(context => ({
|
||||
originSeq: context.originSeq,
|
||||
nodes: context.nodes.map(node => node.seq),
|
||||
}))).toEqual([
|
||||
{ originSeq: undefined, nodes: [baseSeq] },
|
||||
{ originSeq: baseSeq + 1, nodes: [baseSeq + 1] },
|
||||
])
|
||||
})
|
||||
|
||||
it('projects frozen surface generations without widening the core live surface', () => {
|
||||
const events = [
|
||||
ev.user(0, 'a'),
|
||||
ev.user(1, 'b'),
|
||||
at(2, {
|
||||
type: 'assistant/message',
|
||||
surfaceOp: { op: 'replace', start: 0, end: 0 },
|
||||
sourceEventSeqs: [0],
|
||||
data: {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'summary' }],
|
||||
source: { kind: 'model', provider: 'fake', model: 'fake' },
|
||||
}),
|
||||
},
|
||||
}),
|
||||
at(3, {
|
||||
type: 'assistant/message',
|
||||
surfaceOp: { op: 'replace', start: 2, end: 1 },
|
||||
sourceEventSeqs: [2, 1],
|
||||
data: {
|
||||
turn: 1,
|
||||
step: 2,
|
||||
message: createMessage({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'summary 2' }],
|
||||
source: { kind: 'model', provider: 'fake', model: 'fake' },
|
||||
}),
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
expect(projectConversationHistory(events.map(event => ({ event }))).contexts.map(context => ({
|
||||
id: context.id,
|
||||
parentId: context.parentId,
|
||||
originSeq: context.originSeq,
|
||||
nodes: context.nodes.map(node => node.seq),
|
||||
}))).toEqual([
|
||||
{ id: 0, parentId: undefined, originSeq: undefined, nodes: [0, 1] },
|
||||
{ id: 1, parentId: 0, originSeq: 2, nodes: [2, 1] },
|
||||
{ id: 2, parentId: 1, originSeq: 3, nodes: [3] },
|
||||
])
|
||||
})
|
||||
|
||||
it('projects assistant timing and the active request header from history', () => {
|
||||
const projection = projectConversationHistory([
|
||||
ev.stepStart(0, 1, 2),
|
||||
at(1, { type: 'request/header', data: {
|
||||
reason: 'initial',
|
||||
header: {
|
||||
config: { provider: 'fake', model: 'first' },
|
||||
tools: [],
|
||||
},
|
||||
} }),
|
||||
ev.chunkStart(2, 1, 2),
|
||||
ev.chunkText(3, 1, 'token', 2),
|
||||
ev.assistant(4, 1, 'done', 2),
|
||||
ev.stepStart(5, 2, 1),
|
||||
ev.chunkText(6, 2, 'next', 1),
|
||||
ev.assistant(7, 2, 'next done', 1),
|
||||
].map(event => ({ event })))
|
||||
|
||||
expect(projection.eventNodes[0]).toMatchObject({
|
||||
kind: 'assistant',
|
||||
timing: {
|
||||
stepStartTime: 1_700_000_000_000,
|
||||
firstTokenTime: 1_700_000_000_003,
|
||||
completedTime: 1_700_000_000_004,
|
||||
},
|
||||
requestConfig: { provider: 'fake', model: 'first' },
|
||||
})
|
||||
|
||||
expect(projection.eventNodes.at(-1)).toMatchObject({
|
||||
timing: {
|
||||
stepStartTime: 1_700_000_000_005,
|
||||
firstTokenTime: 1_700_000_000_006,
|
||||
completedTime: 1_700_000_000_007,
|
||||
},
|
||||
requestConfig: { provider: 'fake', model: 'first' },
|
||||
})
|
||||
})
|
||||
|
||||
it('projects nested dispatches onto settled and interrupted history calls', () => {
|
||||
const projection = projectConversationHistory([
|
||||
ev.turnStart(0, 1),
|
||||
ev.toolCall(1, 1, 'settled', 'run_code', '{}'),
|
||||
ev.codeDispatchStart(2, 'settled', 1, 'run_code', { code: 'nested' }),
|
||||
ev.codeDispatchStart(3, 'settled:code:1', 1, 'read', { path: 'a.txt' }),
|
||||
ev.codeDispatch(4, 'settled:code:1', 1, 'read', { path: 'a.txt' }, 'alpha'),
|
||||
ev.codeDispatch(5, 'settled', 1, 'run_code', { code: 'nested' }, 'alpha'),
|
||||
ev.toolResult(6, 1, 'settled', 'done'),
|
||||
ev.turnEnd(7, 1),
|
||||
ev.turnStart(8, 2),
|
||||
ev.toolCall(9, 2, 'interrupted', 'run_code', '{}'),
|
||||
ev.codeDispatchStart(10, 'interrupted', 1, 'bash', { command: 'sleep 1' }),
|
||||
ev.turnEnd(11, 2, 'aborted'),
|
||||
].map(event => ({ event })))
|
||||
|
||||
const settled = {
|
||||
callId: 'settled',
|
||||
subCalls: [{
|
||||
callId: 'settled:code:1',
|
||||
subCalls: [{ callId: 'settled:code:1:code:1', call: { name: 'read' } }],
|
||||
}],
|
||||
}
|
||||
expect(projection.eventNodes).toMatchObject([settled])
|
||||
expect(projection.contexts[0]?.nodes).toMatchObject([settled])
|
||||
expect(projection.interruptedNodes).toMatchObject([{
|
||||
callId: 'interrupted',
|
||||
subCalls: [{ callId: 'interrupted:code:1', name: 'bash' }],
|
||||
}])
|
||||
})
|
||||
|
||||
it('drops completed token payloads without changing inspection projections', () => {
|
||||
const events = [
|
||||
ev.user(0, 'before'),
|
||||
ev.stepStart(1, 1, 0),
|
||||
ev.chunkStart(2, 1),
|
||||
ev.chunkText(3, 1, ''),
|
||||
ev.chunkText(4, 1, 'first'),
|
||||
ev.chunkText(5, 1, ' discarded'),
|
||||
at(6, { type: 'assistant/chunk', data: {
|
||||
turn: 1,
|
||||
step: 0,
|
||||
chunk: { type: 'usage', usage: { inputTokens: 4, outputTokens: 2 } },
|
||||
} }),
|
||||
ev.assistant(7, 1, 'first discarded'),
|
||||
ev.compactSummary(8, 'summary', 0, 7),
|
||||
ev.compactCheckpoint(9, 8, 0, 7),
|
||||
ev.stepStart(10, 2, 0),
|
||||
ev.chunkStart(11, 2),
|
||||
ev.chunkText(12, 2, 'interrupted'),
|
||||
ev.turnEnd(13, 2, 'aborted'),
|
||||
]
|
||||
const raw = events.map(event => ({ event }))
|
||||
const compacted = compactHistoryInspectionEntries(raw)
|
||||
|
||||
expect(compacted.map(entry => entry.event.seq)).toEqual([
|
||||
0, 1, 4, 6, 7, 8, 9, 10, 11, 12, 13,
|
||||
])
|
||||
expect(projectConversationHistory(compacted)).toEqual(projectConversationHistory(raw))
|
||||
expect(inspectRequests(compacted)).toEqual(inspectRequests(raw))
|
||||
})
|
||||
})
|
||||
@@ -1,319 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { HistoryEntry } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import { inspectRequests } from '../src/client/sessions/request-inspection.ts'
|
||||
|
||||
const at = (seq: number, type: string, data: unknown): SessionEvent =>
|
||||
({ seq, time: 1_700_000_000_000 + seq, type, data }) as SessionEvent
|
||||
|
||||
const entriesOf = (events: readonly SessionEvent[]): HistoryEntry[] =>
|
||||
events.map(event => ({ event }))
|
||||
|
||||
describe('inspectRequests', () => {
|
||||
it('projects ordinary and compaction calls into one chronological request stream', () => {
|
||||
const events = [
|
||||
at(0, 'step/start', { turn: 1, step: 1 }),
|
||||
at(1, 'request/header', {
|
||||
reason: 'initial',
|
||||
header: {
|
||||
config: { provider: 'fake', model: 'model' },
|
||||
system: 'system',
|
||||
tools: [{
|
||||
name: 'read',
|
||||
description: 'Read a file.',
|
||||
parameters: { type: 'object' },
|
||||
}],
|
||||
},
|
||||
}),
|
||||
at(2, 'tool/call', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
callId: 'call-1',
|
||||
name: 'read',
|
||||
arguments: '{}',
|
||||
}),
|
||||
at(3, 'assistant/message', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
message: createAssistantMessage({
|
||||
content: [{ type: 'text', text: 'done' }],
|
||||
source: { provider: 'fake', model: 'model' },
|
||||
}),
|
||||
usage: { inputTokens: 5, outputTokens: 2 },
|
||||
}),
|
||||
at(4, 'step/end', { turn: 1, step: 1 }),
|
||||
at(5, 'compact/start', { turn: 1 }),
|
||||
at(6, 'compact/summary', {
|
||||
summary: [{ type: 'text', text: 'summary' }],
|
||||
rawOutput: [
|
||||
{ type: 'reasoning', text: 'thought' },
|
||||
{ type: 'text', text: 'summary' },
|
||||
],
|
||||
provider: 'fake',
|
||||
model: 'compact-model',
|
||||
usage: { inputTokens: 8, outputTokens: 3 },
|
||||
}),
|
||||
at(7, 'user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'checkpoint' }],
|
||||
source: { kind: 'plugin', plugin: 'compact' },
|
||||
})),
|
||||
at(8, 'compact/end', { turn: 1 }),
|
||||
]
|
||||
const snapshot = inspectRequests(entriesOf(events))
|
||||
expect(snapshot.requests).toMatchObject([
|
||||
{
|
||||
purpose: 'assistant',
|
||||
startSeq: 0,
|
||||
resultSeq: 3,
|
||||
status: 'complete',
|
||||
prompt: {
|
||||
config: { provider: 'fake', model: 'model' },
|
||||
system: 'system',
|
||||
},
|
||||
promptChange: { seq: 1, kind: 'initial' },
|
||||
},
|
||||
{
|
||||
purpose: 'compaction',
|
||||
startSeq: 5,
|
||||
resultSeq: 6,
|
||||
replacementSeq: 7,
|
||||
status: 'complete',
|
||||
summary: [{ type: 'text', text: 'summary' }],
|
||||
},
|
||||
])
|
||||
expect(snapshot.callSchemas.get('call-1')?.name).toBe('read')
|
||||
})
|
||||
|
||||
it('does not promote a truncated resume or change header to the initial prompt', () => {
|
||||
for (const reason of ['resume', 'change'] as const) {
|
||||
const snapshot = inspectRequests(entriesOf([
|
||||
at(10, 'step/start', { turn: 3, step: 1 }),
|
||||
at(11, 'request/header', {
|
||||
reason,
|
||||
header: {
|
||||
config: { provider: 'fake', model: 'model' },
|
||||
system: 'tail-window prompt',
|
||||
},
|
||||
}),
|
||||
]))
|
||||
|
||||
expect(snapshot.requests[0]).toMatchObject({
|
||||
purpose: 'assistant',
|
||||
prompt: { system: 'tail-window prompt' },
|
||||
})
|
||||
expect(snapshot.requests[0]).not.toHaveProperty('promptChange')
|
||||
}
|
||||
})
|
||||
|
||||
it('classifies a prompt change once the preceding header is loaded', () => {
|
||||
const snapshot = inspectRequests(entriesOf([
|
||||
at(0, 'step/start', { turn: 1, step: 1 }),
|
||||
at(1, 'request/header', {
|
||||
reason: 'initial',
|
||||
header: {
|
||||
config: { provider: 'fake', model: 'model' },
|
||||
system: 'before',
|
||||
},
|
||||
}),
|
||||
at(2, 'step/start', { turn: 1, step: 2 }),
|
||||
at(3, 'request/header', {
|
||||
reason: 'change',
|
||||
header: {
|
||||
config: { provider: 'fake', model: 'model' },
|
||||
system: 'after',
|
||||
},
|
||||
}),
|
||||
]))
|
||||
|
||||
expect(snapshot.requests[1]).toMatchObject({
|
||||
promptChange: {
|
||||
seq: 3,
|
||||
kind: 'system',
|
||||
previous: { system: 'before' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves a standalone compaction owner without widening assistant turns', () => {
|
||||
const snapshot = inspectRequests(entriesOf([
|
||||
at(0, 'compact/start', { turn: null }),
|
||||
at(1, 'compact/summary', {
|
||||
summary: [{ type: 'text', text: 'standalone summary' }],
|
||||
provider: 'fake',
|
||||
model: 'compact-model',
|
||||
}),
|
||||
at(2, 'compact/end', { turn: null }),
|
||||
at(3, 'step/start', { turn: 2, step: 1 }),
|
||||
]))
|
||||
|
||||
const [compaction, assistant] = snapshot.requests
|
||||
expect(compaction).toMatchObject({
|
||||
purpose: 'compaction',
|
||||
turn: null,
|
||||
step: 0,
|
||||
status: 'complete',
|
||||
})
|
||||
expect(assistant).toMatchObject({
|
||||
purpose: 'assistant',
|
||||
turn: 2,
|
||||
step: 1,
|
||||
status: 'running',
|
||||
})
|
||||
if (assistant?.purpose === 'assistant') {
|
||||
const turn: number = assistant.turn
|
||||
expect(turn).toBe(2)
|
||||
}
|
||||
})
|
||||
|
||||
it('interrupts an orphaned compaction at end-seed before projecting a new attempt', () => {
|
||||
const snapshot = inspectRequests(entriesOf([
|
||||
at(0, 'compact/start', { turn: null }),
|
||||
at(1, 'session/end-seed', {}),
|
||||
at(2, 'compact/start', { turn: null }),
|
||||
at(3, 'compact/summary', {
|
||||
summary: [{ type: 'text', text: 'replacement summary' }],
|
||||
provider: 'fake',
|
||||
model: 'compact-model',
|
||||
}),
|
||||
at(4, 'compact/end', { turn: null }),
|
||||
]))
|
||||
|
||||
expect(snapshot.requests).toMatchObject([
|
||||
{
|
||||
purpose: 'compaction',
|
||||
startSeq: 0,
|
||||
status: 'error',
|
||||
completedAt: 1_700_000_000_001,
|
||||
error: 'Compaction was interrupted before completion.',
|
||||
},
|
||||
{
|
||||
purpose: 'compaction',
|
||||
startSeq: 2,
|
||||
status: 'complete',
|
||||
completedAt: 1_700_000_000_004,
|
||||
summary: [{ type: 'text', text: 'replacement summary' }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('captures schemas for nested tool dispatches from the active request header', () => {
|
||||
const snapshot = inspectRequests(entriesOf([
|
||||
at(0, 'request/header', {
|
||||
reason: 'initial',
|
||||
header: {
|
||||
config: { provider: 'fake', model: 'model' },
|
||||
tools: [{
|
||||
name: 'read',
|
||||
description: 'Read a file.',
|
||||
parameters: { type: 'object' },
|
||||
}],
|
||||
},
|
||||
}),
|
||||
at(1, 'tool/code-dispatch-start', {
|
||||
parentCallId: 'parent',
|
||||
subCallId: 'nested',
|
||||
name: 'read',
|
||||
arguments: {},
|
||||
}),
|
||||
]))
|
||||
|
||||
expect(snapshot.callSchemas.get('nested')?.name).toBe('read')
|
||||
})
|
||||
|
||||
it('keeps chunk-reported usage through request failure and prefers it to message fallback', () => {
|
||||
const chunkUsage = { inputTokens: 21, outputTokens: 3 }
|
||||
const retryUsage = {
|
||||
inputTokens: 5,
|
||||
outputTokens: 2,
|
||||
cacheReadTokens: 8,
|
||||
reasoningTokens: 1,
|
||||
}
|
||||
const snapshot = inspectRequests(entriesOf([
|
||||
at(0, 'step/start', { turn: 1, step: 1 }),
|
||||
at(1, 'assistant/chunk', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
chunk: { type: 'usage', usage: chunkUsage },
|
||||
}),
|
||||
at(2, 'llm/retry', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
retry: 1,
|
||||
maxRetries: 2,
|
||||
delayMs: 100,
|
||||
failure: { message: 'rate limited' },
|
||||
}),
|
||||
at(3, 'assistant/chunk', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
chunk: { type: 'usage', usage: retryUsage },
|
||||
}),
|
||||
at(4, 'assistant/message', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
message: createAssistantMessage({
|
||||
content: [{ type: 'text', text: 'recovered' }],
|
||||
source: { provider: 'fake', model: 'model' },
|
||||
}),
|
||||
usage: { inputTokens: 1, outputTokens: 1 },
|
||||
}),
|
||||
]))
|
||||
|
||||
expect(snapshot.requests[0]).toMatchObject({
|
||||
status: 'complete',
|
||||
usage: {
|
||||
inputTokens: 26,
|
||||
outputTokens: 5,
|
||||
cacheReadTokens: 8,
|
||||
reasoningTokens: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps provider credential fragments out of projected request errors', () => {
|
||||
const snapshot = inspectRequests(entriesOf([
|
||||
at(0, 'step/start', { turn: 1, step: 1 }),
|
||||
at(1, 'turn/end', {
|
||||
turn: 1, reason: { kind: 'error', error: {
|
||||
code: 'AUTH',
|
||||
message: 'Authentication Fails, Your api key: sk-preview-secret is invalid',
|
||||
},
|
||||
},
|
||||
}),
|
||||
at(2, 'step/start', { turn: 2, step: 1 }),
|
||||
at(3, 'turn/end', {
|
||||
turn: 2, reason: { kind: 'error', error: { message: 'plugin exploded', code: 'UNKNOWN' } },
|
||||
}),
|
||||
]))
|
||||
|
||||
expect(snapshot.requests).toMatchObject([
|
||||
{ status: 'error', error: 'API key is invalid' },
|
||||
{ status: 'error', error: 'plugin exploded' },
|
||||
])
|
||||
})
|
||||
|
||||
it('treats a scrubbed durable-fixture tool catalog as unavailable', () => {
|
||||
const snapshot = inspectRequests(entriesOf([
|
||||
at(0, 'step/start', { turn: 1, step: 1 }),
|
||||
at(1, 'request/header', {
|
||||
reason: 'initial',
|
||||
header: {
|
||||
config: { provider: 'fake', model: 'model' },
|
||||
tools: '{{tools}}',
|
||||
},
|
||||
}),
|
||||
at(2, 'tool/call', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
callId: 'call-1',
|
||||
name: 'read',
|
||||
arguments: '{}',
|
||||
}),
|
||||
]))
|
||||
|
||||
expect(snapshot.callSchemas).toEqual(new Map())
|
||||
const [request] = snapshot.requests
|
||||
expect(request?.purpose === 'assistant' ? request.prompt?.tools : undefined).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,180 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { SessionHistorySource } from '../src/client/session-history/source.ts'
|
||||
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
|
||||
import { entries, ev, plainTurn } from './event-script.ts'
|
||||
|
||||
const SID = 'history-s1' as SessionId
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
function histResponse(events: SessionEvent[], hasMore = false) {
|
||||
return Promise.resolve(ok({ events: entries(events) as never[], hasMore }))
|
||||
}
|
||||
|
||||
describe('SessionHistorySource', () => {
|
||||
it('loads the tail first and prepends older pages on demand', async () => {
|
||||
const pages = [
|
||||
plainTurn(0, 0, '最早问', '最早答'),
|
||||
plainTurn(6, 1, '中间问', '中间答'),
|
||||
plainTurn(12, 2, '最新问', '最新答'),
|
||||
]
|
||||
const api = new FakeApiClient()
|
||||
api.onHistory = (payload) => {
|
||||
if (payload.beforeSeq === undefined) return histResponse(pages[2]!, true)
|
||||
if (payload.beforeSeq === 12) return histResponse(pages[1]!, true)
|
||||
return histResponse(pages[0]!, false)
|
||||
}
|
||||
const source = new SessionHistorySource(SID, api)
|
||||
|
||||
await source.loadTail()
|
||||
|
||||
expect(api.callsOf('session.history')).toHaveLength(1)
|
||||
expect(source.getSnapshot().hasMore).toBe(true)
|
||||
expect(source.getSnapshot().baseSeq).toBe(12)
|
||||
expect(source.getSnapshot().inspection.eventNodes.map(node => node.seq))
|
||||
.toEqual([13, 15])
|
||||
|
||||
expect(await source.loadOlder()).toBe(true)
|
||||
expect(await source.loadOlder()).toBe(true)
|
||||
expect(await source.loadOlder()).toBe(false)
|
||||
|
||||
expect(api.callsOf('session.history')).toHaveLength(3)
|
||||
expect(source.getSnapshot().hasMore).toBe(false)
|
||||
expect(source.getSnapshot().baseSeq).toBe(0)
|
||||
expect(source.getSnapshot().inspection.eventNodes.map(node => node.seq))
|
||||
.toEqual([1, 3, 7, 9, 13, 15])
|
||||
})
|
||||
|
||||
it('pins a lazy inspection to the entries in its source snapshot', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onHistory = () => histResponse(plainTurn(0, 0, '问', '答'))
|
||||
const source = new SessionHistorySource(SID, api)
|
||||
await source.loadTail()
|
||||
const before = source.getSnapshot()
|
||||
|
||||
source.handleMuxFrame({
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event: ev.user(6, 'later'),
|
||||
})
|
||||
|
||||
expect(before.inspection.eventNodes.map(node => node.seq)).toEqual([1, 3])
|
||||
expect(source.getSnapshot().inspection.eventNodes.map(node => node.seq))
|
||||
.toEqual([1, 3, 6])
|
||||
})
|
||||
|
||||
it('publishes multiple assistant chunks once per browser frame', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onHistory = () => histResponse(plainTurn(0, 0, '问', '答'))
|
||||
const source = new SessionHistorySource(SID, api)
|
||||
await source.loadTail()
|
||||
const frames: FrameRequestCallback[] = []
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
||||
frames.push(callback)
|
||||
return frames.length
|
||||
})
|
||||
let notifications = 0
|
||||
const unsubscribe = source.subscribe(() => { notifications++ })
|
||||
const before = source.getSnapshot().inspection
|
||||
const finalizedNodes = before.eventNodes
|
||||
const requests = before.requests
|
||||
const contexts = before.contexts
|
||||
|
||||
for (const event of [
|
||||
ev.chunkStart(6, 1),
|
||||
ev.chunkText(7, 1, 'stream '),
|
||||
ev.chunkText(8, 1, 'content'),
|
||||
]) {
|
||||
source.handleMuxFrame({
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event,
|
||||
})
|
||||
}
|
||||
|
||||
expect(frames).toHaveLength(1)
|
||||
expect(notifications).toBe(0)
|
||||
frames[0]?.(0)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(notifications).toBe(1)
|
||||
const streamed = source.getSnapshot().inspection
|
||||
expect(streamed.eventNodes).toBe(finalizedNodes)
|
||||
expect(streamed.requests).toBe(requests)
|
||||
expect(streamed.contexts).toBe(contexts)
|
||||
expect(streamed.partial?.blocks).toEqual([
|
||||
{ kind: 'text', text: 'stream content' },
|
||||
])
|
||||
|
||||
source.handleMuxFrame({
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event: ev.chunkText(9, 1, ' then final'),
|
||||
})
|
||||
source.handleMuxFrame({
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event: ev.assistant(10, 1, 'stream content then final'),
|
||||
})
|
||||
await Promise.resolve()
|
||||
|
||||
expect(notifications).toBe(2)
|
||||
const finalized = source.getSnapshot().inspection
|
||||
expect(finalized.eventNodes).not.toBe(finalizedNodes)
|
||||
expect(finalized.partial).toBeNull()
|
||||
frames[1]?.(0)
|
||||
await Promise.resolve()
|
||||
expect(notifications).toBe(2)
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
it('stops loading when an older page fails to advance', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onHistory = payload => payload.beforeSeq === undefined
|
||||
? histResponse(plainTurn(6, 1, '新问', '新答'), true)
|
||||
: Promise.resolve(err({
|
||||
code: 'internal',
|
||||
message: 'page unavailable',
|
||||
details: {},
|
||||
}))
|
||||
const source = new SessionHistorySource(SID, api)
|
||||
|
||||
await source.loadTail()
|
||||
expect(await source.loadOlder()).toBe(false)
|
||||
|
||||
expect(api.callsOf('session.history')).toHaveLength(2)
|
||||
expect(source.getSnapshot().hasMore).toBe(true)
|
||||
})
|
||||
|
||||
it('finishes an already started older page after consumer cancellation', async () => {
|
||||
const middle = deferred<Awaited<ReturnType<FakeApiClient['onHistory']>>>()
|
||||
const olderStarted = deferred<undefined>()
|
||||
const api = new FakeApiClient()
|
||||
api.onHistory = (payload) => {
|
||||
if (payload.beforeSeq === undefined) {
|
||||
return histResponse(plainTurn(12, 2, '最新问', '最新答'), true)
|
||||
}
|
||||
olderStarted.resolve(undefined)
|
||||
return middle.promise
|
||||
}
|
||||
const source = new SessionHistorySource(SID, api)
|
||||
const controller = new AbortController()
|
||||
await source.loadTail(controller.signal)
|
||||
const complete = source.loadOlder(controller.signal)
|
||||
await olderStarted.promise
|
||||
controller.abort()
|
||||
middle.resolve(ok({
|
||||
events: entries(plainTurn(6, 1, '中间问', '中间答')) as never[],
|
||||
hasMore: true,
|
||||
}))
|
||||
|
||||
expect(await complete).toBe(true)
|
||||
|
||||
expect(api.callsOf('session.history')).toHaveLength(2)
|
||||
expect(source.getSnapshot().hasMore).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -123,12 +123,13 @@ function testViewDefinition(): ConversationViewDefinition<ChatConversationViewNo
|
||||
|
||||
const TEST_EVENT_DEFINITION: ConversationNodeDefinition<TestEventState> = {
|
||||
kind: 'runtime-test-event',
|
||||
target: 'chat',
|
||||
match: event => ({ id: String(event.seq), role: 'start' }),
|
||||
start: (_context, match) => ({ event: match.event, view: match.view }),
|
||||
update: context => context.state,
|
||||
publication: match => match.event.type === 'assistant/chunk' ? 'animation-frame' : 'immediate',
|
||||
buildViewNode: (context, target) => {
|
||||
if (target !== 'chat' || context.state === undefined || context.start === undefined) return null
|
||||
buildViewNode: (context) => {
|
||||
if (context.state === undefined || context.start === undefined) return null
|
||||
return {
|
||||
key: context.key,
|
||||
kind: 'runtime-test-event',
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-schema-form",
|
||||
"description": "Schema/draft model layer for settings editors: rehydrates a serialized schemastery schema, validates drafts, and edits them immutably by path",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/schema-form"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -20,15 +27,15 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-test-runtime",
|
||||
"description": "jsdom slot test runtime: real Cordis Context + SlotsService + web-react renderer with test-owned session/workspace doubles for feature specs",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/test-runtime"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -25,12 +32,12 @@
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-web-react": "^0.0.1",
|
||||
"@deepseek-ai/dsh-host-apiproxy": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
@@ -42,7 +49,7 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@types/react-dom": "~18.3.0",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import type {
|
||||
ConversationSnapshot, ISession, SessionId, SessionSummary, WorkspaceListState,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { EMPTY_CHAT_SNAPSHOT } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/**
|
||||
* Fixture overrides for the session behavior face: any subset of the
|
||||
@@ -46,6 +48,7 @@ export interface SessionFixture {
|
||||
export function conversationSnapshot(sessionId: SessionId): ConversationSnapshot {
|
||||
return {
|
||||
sessionId,
|
||||
views: EMPTY_CONVERSATION_VIEWS,
|
||||
chat: EMPTY_CHAT_SNAPSHOT,
|
||||
nodes: [],
|
||||
turnTimings: new Map(),
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-agent-preset",
|
||||
"description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-agent-preset"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -40,16 +47,16 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-connection": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-web-react": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -64,7 +71,7 @@
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 180px;
|
||||
padding: 0 8px;
|
||||
padding: 0 2px 0 0;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
background: var(--dsw-alias-fill-tsp-secondary);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { IconThinkOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { IconAgentPresetOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
// Type-only: pulls the ui-conversation SlotMap merge (the header actions).
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { AgentPresetSettingsState } from './settings-store.ts'
|
||||
@@ -57,7 +57,7 @@ export function AgentPresetLabel({
|
||||
const text = option === undefined ? undefined : presetDisplayText(option, t)
|
||||
return (
|
||||
<span className={css.label} title={text?.description ?? t('headerHint')}>
|
||||
<IconThinkOutline16 className={css.icon} />
|
||||
<IconAgentPresetOutline16 size={14} className={css.icon} />
|
||||
{text?.name ?? preset}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
min-height: 28px;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
border-radius: 16px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font-size: 13px;
|
||||
@@ -36,6 +36,61 @@
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
/* Introduce cue: the icon eases in on an overshoot-free expo curve (duration
|
||||
matches INTRO_TEXT_DELAY_MS, so the characters start the moment it lands),
|
||||
then the name's characters fade up on a stagger (delays set inline per
|
||||
character). All chars occupy their width from the start, so nothing
|
||||
reflows mid-run. */
|
||||
.introIcon {
|
||||
animation: seat-icon-in 0.15s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
@keyframes seat-icon-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wraps the staggered characters into one flex item, so the chip's gap
|
||||
applies around the name as a whole rather than between characters. */
|
||||
.introText {
|
||||
display: inline-block;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.introChar {
|
||||
display: inline-block;
|
||||
white-space: pre;
|
||||
opacity: 0;
|
||||
animation: seat-char-in 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes seat-char-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.introIcon,
|
||||
.introChar {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
flex: none;
|
||||
color: var(--dsw-alias-label-caption);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { IconChevronDownOutline14, IconThinkOutline16, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { IconAgentPresetOutline16, IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
// Type-only: pulls the ui-conversation SlotMap merge (the hero seat).
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { AgentPresetSeatState } from './seat-store.ts'
|
||||
@@ -32,6 +32,29 @@ export interface AgentPresetSeatInjected {
|
||||
load: () => Promise<void>
|
||||
/** Stage one preset for the next session. */
|
||||
select: (id: string) => Promise<void>
|
||||
/** Clear the one-shot introduce cue once the chip has played it. */
|
||||
introduced: () => void
|
||||
}
|
||||
|
||||
/* Introduce timeline: the icon eases in first (the CSS animation shares this
|
||||
duration); the name's characters start fading up the moment it lands, each
|
||||
taking the fade duration to settle. The cue clears after the last one. The
|
||||
stagger is capped twice: per tick for short CJK names, and by one shared
|
||||
reveal window so a long Latin name finishes in the same time as its CJK
|
||||
counterpart instead of dragging the run out per character. */
|
||||
const INTRO_TEXT_DELAY_MS = 150
|
||||
const INTRO_CHAR_STAGGER_MS = 40
|
||||
const INTRO_TEXT_REVEAL_MS = 200
|
||||
const INTRO_CHAR_FADE_MS = 400
|
||||
|
||||
/**
|
||||
* Per-character start offset for the introduce reveal.
|
||||
* @param count - character count of the shown preset name.
|
||||
* @returns milliseconds between successive character starts.
|
||||
*/
|
||||
function introStaggerMs(count: number): number {
|
||||
if (count <= 1) return 0
|
||||
return Math.min(INTRO_CHAR_STAGGER_MS, INTRO_TEXT_REVEAL_MS / (count - 1))
|
||||
}
|
||||
|
||||
/** Full component props. */
|
||||
@@ -45,7 +68,7 @@ export type AgentPresetSeatProps =
|
||||
* @param props - composed slot props.
|
||||
* @returns the chip, or null when the deployment composes no presets.
|
||||
*/
|
||||
export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPresetSeatProps) {
|
||||
export function AgentPresetSeat({ load, select, introduced, useAgentPresetSeat, t }: AgentPresetSeatProps) {
|
||||
const state = useAgentPresetSeat(snapshot => snapshot)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@@ -53,12 +76,54 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
|
||||
void load()
|
||||
}, [load])
|
||||
|
||||
// Nothing to choose between: the deployment composes no presets and every
|
||||
// session shares the host composition.
|
||||
if (state.options.length === 0 || state.current === '') return null
|
||||
|
||||
const chosen = state.options.find(option => option.id === state.current)
|
||||
const chosenText = chosen === undefined ? undefined : presetDisplayText(chosen, t)
|
||||
const label = chosenText?.name ?? state.current
|
||||
const ready = state.options.length > 0 && state.current !== ''
|
||||
|
||||
// The introduce cue: the pick was staged from another screen (the settings
|
||||
// creator entry), so the chip announces it — the icon eases in and each
|
||||
// character of the name fades up on a stagger (CSS owns the motion; this
|
||||
// effect only arms it and acknowledges the cue once the run is over).
|
||||
const [introducing, setIntroducing] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!state.introduce || !ready) return
|
||||
const characters = Array.from(label)
|
||||
if (characters.length === 0 || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
introduced()
|
||||
return
|
||||
}
|
||||
setIntroducing(true)
|
||||
const done = window.setTimeout(() => {
|
||||
setIntroducing(false)
|
||||
introduced()
|
||||
}, INTRO_TEXT_DELAY_MS + (characters.length - 1) * introStaggerMs(characters.length) + INTRO_CHAR_FADE_MS)
|
||||
return () => { window.clearTimeout(done) }
|
||||
}, [state.introduce, ready, label, introduced])
|
||||
|
||||
// Nothing to choose between: the deployment composes no presets and every
|
||||
// session shares the host composition.
|
||||
if (!ready) return null
|
||||
|
||||
// One wrapper span: the chip is a flex row with a gap, so loose character
|
||||
// spans would each pick up the gap between them.
|
||||
const characters = Array.from(label)
|
||||
const stagger = introStaggerMs(characters.length)
|
||||
const shownLabel = introducing
|
||||
? (
|
||||
<span className={css.introText}>
|
||||
{characters.map((character, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={css.introChar}
|
||||
style={{ animationDelay: `${INTRO_TEXT_DELAY_MS + index * stagger}ms` }}
|
||||
>
|
||||
{character}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
: label
|
||||
|
||||
return (
|
||||
<Menu
|
||||
@@ -95,8 +160,8 @@ export function AgentPresetSeat({ load, select, useAgentPresetSeat, t }: AgentPr
|
||||
disabled={state.busy}
|
||||
onClick={() => { setOpen(value => !value) }}
|
||||
>
|
||||
<IconThinkOutline16 className={css.seatIcon} />
|
||||
{chosenText?.name ?? state.current}
|
||||
<IconAgentPresetOutline16 className={introducing ? `${css.seatIcon} ${css.introIcon}` : css.seatIcon} />
|
||||
{shownLabel}
|
||||
<IconChevronDownOutline14 className={css.chevron} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Group-to-group breathing room: the section's 12px gap plus 20px reads the
|
||||
two rosters as separate blocks (32px total). */
|
||||
.group + .group {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.groupHead {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
@@ -363,6 +369,7 @@
|
||||
create button vacated. Dashed like the Models page's add affordances: it
|
||||
reads as a place a preset will appear, not a command. */
|
||||
.creatorButton {
|
||||
box-sizing: border-box;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -372,17 +379,18 @@
|
||||
border: 1px dashed var(--dsw-alias-border-l3);
|
||||
border-radius: 12px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.creatorButton:hover:not(:disabled) {
|
||||
background: var(--dsw-alias-bg-layer-1);
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.creatorButton:disabled {
|
||||
opacity: 0.5;
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -171,6 +171,30 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
)
|
||||
}
|
||||
|
||||
/* The guided alternative to copying: the self-referential preset can
|
||||
read this very composition and author a new one in conversation.
|
||||
Offered only where that preset is actually on the roster and a
|
||||
session can be landed; without a writable root the draft could
|
||||
never be discovered, so the reason rides the disabled button. */
|
||||
const creatorButton = props.startCreatorDraft !== undefined && state.rows.some(row => row.id === 'cordis')
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.creatorButton}
|
||||
disabled={!state.authorable}
|
||||
title={state.authorable ? undefined : t('duplicateUnavailable')}
|
||||
onClick={() => {
|
||||
props.startCreatorDraft?.()
|
||||
props.close()
|
||||
}}
|
||||
>
|
||||
{/* Same glyph as the Models page's add affordances. */}
|
||||
<IconPlusOutline16 size={14} />
|
||||
{t('creatorDraft')}
|
||||
</button>
|
||||
)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className={css.section}>
|
||||
<h2 className={css.title}>{t('nav')}</h2>
|
||||
@@ -180,147 +204,130 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
|
||||
const group = state.rows
|
||||
.filter(row => row.trust === trust)
|
||||
.map(row => ({ row, text: presetDisplayText(row, t) }))
|
||||
if (group.length === 0) return null
|
||||
// The custom group is where a preset of one's own will appear, so it
|
||||
// stays on screen even while empty: heading plus the creator entry.
|
||||
const tail = trust === 'user' ? creatorButton : null
|
||||
if (group.length === 0 && tail === null) return null
|
||||
return (
|
||||
<section key={trust} className={css.group}>
|
||||
<h3 className={css.groupHead}>{heading}</h3>
|
||||
<ul className={css.cards}>
|
||||
{group.map(({ row, text }) => (
|
||||
<li
|
||||
key={row.id}
|
||||
className={row.broken !== undefined
|
||||
? `${css.card} ${css.cardBroken}`
|
||||
: row.isDefault ? `${css.card} ${css.cardActive}` : css.card}
|
||||
>
|
||||
{/* The card body IS the control: picking a preset is the
|
||||
{group.length === 0 ? null : (
|
||||
<ul className={css.cards}>
|
||||
{group.map(({ row, text }) => (
|
||||
<li
|
||||
key={row.id}
|
||||
className={row.broken !== undefined
|
||||
? `${css.card} ${css.cardBroken}`
|
||||
: row.isDefault ? `${css.card} ${css.cardActive}` : css.card}
|
||||
>
|
||||
{/* The card body IS the control: picking a preset is the
|
||||
common act, so it should not hide behind a small button.
|
||||
The action row sits outside it — nesting buttons is
|
||||
invalid, and these act on the card rather than select it.
|
||||
A broken preset cannot compose a session, so its body is
|
||||
disabled and the card says why instead of offering it. */}
|
||||
<button
|
||||
type="button"
|
||||
className={css.cardMain}
|
||||
aria-pressed={row.isDefault}
|
||||
disabled={row.isDefault || row.broken !== undefined}
|
||||
// Without this the name is the whole card read aloud —
|
||||
// title, badge, description, id.
|
||||
aria-label={`${row.broken !== undefined ? t('brokenBadge') : row.isDefault ? t('inUse') : t('setDefault')}: ${text.name}`}
|
||||
title={row.broken ?? (row.isDefault ? t('inUse') : t('setDefault'))}
|
||||
onClick={() => { void props.makeDefault(row.id) }}
|
||||
>
|
||||
<span className={css.cardHead}>
|
||||
<span className={css.cardName}>{text.name}</span>
|
||||
{row.broken !== undefined
|
||||
? <span className={css.brokenBadge}>{t('brokenBadge')}</span>
|
||||
: null}
|
||||
<span className={css.badge}>
|
||||
{row.trust === 'user' ? t('userTrust') : t('builtIn')}
|
||||
<button
|
||||
type="button"
|
||||
className={css.cardMain}
|
||||
aria-pressed={row.isDefault}
|
||||
disabled={row.isDefault || row.broken !== undefined}
|
||||
// Without this the name is the whole card read aloud —
|
||||
// title, badge, description, id.
|
||||
aria-label={`${row.broken !== undefined ? t('brokenBadge') : row.isDefault ? t('inUse') : t('setDefault')}: ${text.name}`}
|
||||
title={row.broken ?? (row.isDefault ? t('inUse') : t('setDefault'))}
|
||||
onClick={() => { void props.makeDefault(row.id) }}
|
||||
>
|
||||
<span className={css.cardHead}>
|
||||
<span className={css.cardName}>{text.name}</span>
|
||||
{row.broken !== undefined
|
||||
? <span className={css.brokenBadge}>{t('brokenBadge')}</span>
|
||||
: null}
|
||||
<span className={css.badge}>
|
||||
{row.trust === 'user' ? t('userTrust') : t('builtIn')}
|
||||
</span>
|
||||
{row.isDefault ? <span className={css.inUse}>{t('inUse')}</span> : null}
|
||||
</span>
|
||||
{row.isDefault ? <span className={css.inUse}>{t('inUse')}</span> : null}
|
||||
</span>
|
||||
<span className={css.cardDesc}>{text.description ?? t('noDescription')}</span>
|
||||
{row.broken === undefined
|
||||
? null
|
||||
: <span className={css.cardBrokenReason} role="alert">{row.broken}</span>}
|
||||
<code className={css.cardId}>{row.id}</code>
|
||||
</button>
|
||||
<div className={css.cardFoot}>
|
||||
{/* Shipped presets are the compositions a copy starts
|
||||
<span className={css.cardDesc}>{text.description ?? t('noDescription')}</span>
|
||||
{row.broken === undefined
|
||||
? null
|
||||
: <span className={css.cardBrokenReason} role="alert">{row.broken}</span>}
|
||||
<code className={css.cardId}>{row.id}</code>
|
||||
</button>
|
||||
<div className={css.cardFoot}>
|
||||
{/* Shipped presets are the compositions a copy starts
|
||||
from, so READING one is the point; a custom preset is
|
||||
edited in its files instead, which the location action
|
||||
leads to. A broken shipped preset has no readable
|
||||
composition to offer, so its viewer is withheld; a
|
||||
broken custom one keeps the location action — the
|
||||
files are where it gets fixed. */}
|
||||
{row.trust === 'system'
|
||||
? row.broken === undefined
|
||||
? (
|
||||
{row.trust === 'system'
|
||||
? row.broken === undefined
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
data-tip={t('view')}
|
||||
aria-label={`${t('view')}: ${text.name}`}
|
||||
onClick={() => { void props.view(row.id) }}
|
||||
>
|
||||
<IconBrowseOutline16 />
|
||||
</button>
|
||||
)
|
||||
: null
|
||||
: (
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
data-tip={t('view')}
|
||||
aria-label={`${t('view')}: ${text.name}`}
|
||||
onClick={() => { void props.view(row.id) }}
|
||||
data-tip={state.hasDocument ? t('openLocation') : t('showLocation')}
|
||||
aria-label={`${state.hasDocument ? t('openLocation') : t('showLocation')}: ${text.name}`}
|
||||
onClick={() => { void props.openLocation(row.id) }}
|
||||
>
|
||||
<IconBrowseOutline16 />
|
||||
<IconFolderOpenOutline16 />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
disabled={!state.authorable || row.broken !== undefined}
|
||||
data-tip={row.broken !== undefined
|
||||
? t('brokenNoCopy')
|
||||
: state.authorable ? t('duplicate') : t('duplicateUnavailable')}
|
||||
aria-label={`${t('duplicate')}: ${text.name}`}
|
||||
onClick={() => { props.beginCopy(row.id) }}
|
||||
>
|
||||
<IconCopyOutline16 />
|
||||
</button>
|
||||
{row.trust === 'user'
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className={`${css.iconButton} ${css.iconDanger}`}
|
||||
data-tip={t('delete')}
|
||||
aria-label={`${t('delete')}: ${text.name}`}
|
||||
onClick={() => { props.confirmDelete(row.id) }}
|
||||
>
|
||||
<IconTrashOutline16 />
|
||||
</button>
|
||||
)
|
||||
: null
|
||||
: null}
|
||||
</div>
|
||||
{state.revealedPaths[row.id] === undefined
|
||||
? null
|
||||
: (
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
data-tip={state.hasDocument ? t('openLocation') : t('showLocation')}
|
||||
aria-label={`${state.hasDocument ? t('openLocation') : t('showLocation')}: ${text.name}`}
|
||||
onClick={() => { void props.openLocation(row.id) }}
|
||||
>
|
||||
<IconFolderOpenOutline16 />
|
||||
</button>
|
||||
<p className={css.revealedPath}>
|
||||
<span className={css.revealedPathLabel}>{t('revealedPathLabel')}</span>
|
||||
<code>{state.revealedPaths[row.id]}</code>
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
disabled={!state.authorable || row.broken !== undefined}
|
||||
data-tip={row.broken !== undefined
|
||||
? t('brokenNoCopy')
|
||||
: state.authorable ? t('duplicate') : t('duplicateUnavailable')}
|
||||
aria-label={`${t('duplicate')}: ${text.name}`}
|
||||
onClick={() => { props.beginCopy(row.id) }}
|
||||
>
|
||||
<IconCopyOutline16 />
|
||||
</button>
|
||||
{row.trust === 'user'
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className={`${css.iconButton} ${css.iconDanger}`}
|
||||
data-tip={t('delete')}
|
||||
aria-label={`${t('delete')}: ${text.name}`}
|
||||
onClick={() => { props.confirmDelete(row.id) }}
|
||||
>
|
||||
<IconTrashOutline16 />
|
||||
</button>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
{state.revealedPaths[row.id] === undefined
|
||||
? null
|
||||
: (
|
||||
<p className={css.revealedPath}>
|
||||
<span className={css.revealedPathLabel}>{t('revealedPathLabel')}</span>
|
||||
<code>{state.revealedPaths[row.id]}</code>
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{tail}
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
{/* The guided alternative to copying: the self-referential preset can
|
||||
read this very composition and author a new one in conversation.
|
||||
Offered only where that preset is actually on the roster and a
|
||||
session can be landed; without a writable root the draft could
|
||||
never be discovered, so the reason rides the disabled button. */}
|
||||
{props.startCreatorDraft !== undefined && state.rows.some(row => row.id === 'cordis')
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.creatorButton}
|
||||
disabled={!state.authorable}
|
||||
title={state.authorable ? undefined : t('duplicateUnavailable')}
|
||||
onClick={() => {
|
||||
props.startCreatorDraft?.()
|
||||
props.close()
|
||||
}}
|
||||
>
|
||||
{/* Same glyph as the Models page's add affordances. */}
|
||||
<IconPlusOutline16 size={14} />
|
||||
{t('creatorDraft')}
|
||||
</button>
|
||||
)
|
||||
: null}
|
||||
<CopyDialog
|
||||
state={state}
|
||||
t={t}
|
||||
|
||||
@@ -114,6 +114,7 @@ export function apply(ctx: ClientContext): void {
|
||||
hooks: { agentPresetSeat: seat.store },
|
||||
load: () => seat.load(),
|
||||
select: (id: string) => seat.select(id),
|
||||
introduced: () => { seat.introduced() },
|
||||
})
|
||||
|
||||
const labelInjected = (): AgentPresetLabelInjected => ({
|
||||
@@ -146,7 +147,9 @@ export function apply(ctx: ClientContext): void {
|
||||
// on: the chip's list-change applier composes the blank session the
|
||||
// workspace connect produces or reuses.
|
||||
creatorDraft = () => {
|
||||
seat.stage('cordis')
|
||||
// The introduce cue makes the chip announce the pick the user never
|
||||
// made on this screen — the stage happened back in settings.
|
||||
seat.stage('cordis', true)
|
||||
scope.workspaces.startSession()
|
||||
}
|
||||
const chip = scope.slots.register({
|
||||
|
||||
@@ -26,10 +26,16 @@ export interface AgentPresetSeatState {
|
||||
/** A rejected apply's message, cleared by the next attempt. */
|
||||
error: string | null
|
||||
busy: boolean
|
||||
/**
|
||||
* One-shot cue that the chip should introduce itself (the creator-draft
|
||||
* entry staged the pick from another screen, so the user never touched the
|
||||
* chip); the renderer clears it via `introduced()` once played.
|
||||
*/
|
||||
introduce: boolean
|
||||
}
|
||||
|
||||
const INITIAL: AgentPresetSeatState = {
|
||||
options: [], current: '', error: null, busy: false,
|
||||
options: [], current: '', error: null, busy: false, introduce: false,
|
||||
}
|
||||
|
||||
/** One session's identity and whether it has started. */
|
||||
@@ -121,10 +127,18 @@ export class AgentPresetSeatController {
|
||||
* list-change applier, which fires when the started session becomes
|
||||
* current.
|
||||
* @param id - the preset to stage.
|
||||
* @param introduce - true when the stage came from another screen and the
|
||||
* chip should announce itself on the session it lands on.
|
||||
*/
|
||||
stage(id: string): void {
|
||||
stage(id: string, introduce = false): void {
|
||||
this.staged = id
|
||||
this.set({ current: id, error: null })
|
||||
this.set({ current: id, error: null, introduce })
|
||||
}
|
||||
|
||||
/** Acknowledge the introduction cue once the chip has played it. */
|
||||
introduced(): void {
|
||||
if (!this.store.getSnapshot().introduce) return
|
||||
this.set({ introduce: false })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -496,6 +496,15 @@ describe('ui-agent-preset apply', () => {
|
||||
expect(section.startCreatorDraft).toBeDefined()
|
||||
expect(seat.hooks.agentPresetSeat.getSnapshot().current).toBe('cordis')
|
||||
expect(workspaces.starts).toHaveLength(1)
|
||||
|
||||
// A cross-screen stage carries the introduce cue; the chip acknowledges
|
||||
// it once, and a repeat acknowledgement leaves the snapshot untouched.
|
||||
expect(seat.hooks.agentPresetSeat.getSnapshot().introduce).toBe(true)
|
||||
seat.introduced()
|
||||
const acknowledged = seat.hooks.agentPresetSeat.getSnapshot()
|
||||
expect(acknowledged.introduce).toBe(false)
|
||||
seat.introduced()
|
||||
expect(seat.hooks.agentPresetSeat.getSnapshot()).toBe(acknowledged)
|
||||
conversation()
|
||||
})
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ const SEAT_READY: AgentPresetSeatState = {
|
||||
],
|
||||
busy: false,
|
||||
error: null,
|
||||
introduce: false,
|
||||
}
|
||||
|
||||
function renderRow(state: Partial<AgentPresetSettingsState> = {}) {
|
||||
@@ -56,7 +57,11 @@ function renderRow(state: Partial<AgentPresetSettingsState> = {}) {
|
||||
|
||||
function renderSeat(state: Partial<AgentPresetSeatState> = {}) {
|
||||
const store = createSnapshotStore<AgentPresetSeatState>({ ...SEAT_READY, ...state })
|
||||
const actions = { load: vi.fn(() => Promise.resolve()), select: vi.fn(() => Promise.resolve()) }
|
||||
const actions = {
|
||||
load: vi.fn(() => Promise.resolve()),
|
||||
select: vi.fn(() => Promise.resolve()),
|
||||
introduced: vi.fn(),
|
||||
}
|
||||
render(<AgentPresetSeat {...({
|
||||
...actions,
|
||||
useAgentPresetSeat: bindSnapshotSelector(store),
|
||||
@@ -272,6 +277,94 @@ describe('the new-session chip', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('the chip introduce cue', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** Character spans carry inline animation delays; nothing else does. */
|
||||
function delayedChars(): HTMLElement[] {
|
||||
return Array.from(screen.getByRole('button').querySelectorAll<HTMLElement>('[style]'))
|
||||
}
|
||||
|
||||
it('reveals a long Latin name inside the shared window, then acknowledges', () => {
|
||||
vi.stubGlobal('matchMedia', vi.fn(() => ({ matches: false })))
|
||||
vi.useFakeTimers()
|
||||
const actions = renderSeat({
|
||||
current: 'creator',
|
||||
options: [{ id: 'creator', trust: 'user', name: 'CreatorMode' }],
|
||||
introduce: true,
|
||||
})
|
||||
|
||||
// Eleven characters split the 200ms window into 20ms steps, where the
|
||||
// fixed 40ms tick would have doubled the run for a Latin name.
|
||||
const chars = delayedChars()
|
||||
expect(chars.map(span => span.textContent).join('')).toBe('CreatorMode')
|
||||
expect(chars[0]!.style.animationDelay).toBe('150ms')
|
||||
expect(chars[1]!.style.animationDelay).toBe('170ms')
|
||||
expect(chars[10]!.style.animationDelay).toBe('350ms')
|
||||
|
||||
// 150 delay + 200 window + 400 fade: acknowledged only once the last
|
||||
// character has settled, and the label is plain text again after.
|
||||
act(() => { vi.advanceTimersByTime(749) })
|
||||
expect(actions.introduced).not.toHaveBeenCalled()
|
||||
act(() => { vi.advanceTimersByTime(1) })
|
||||
expect(actions.introduced).toHaveBeenCalledTimes(1)
|
||||
expect(delayedChars()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('keeps the per-tick cap for a short CJK name', () => {
|
||||
vi.stubGlobal('matchMedia', vi.fn(() => ({ matches: false })))
|
||||
vi.useFakeTimers()
|
||||
renderSeat({
|
||||
current: 'creator',
|
||||
options: [{ id: 'creator', trust: 'user', name: '创造模式' }],
|
||||
introduce: true,
|
||||
})
|
||||
|
||||
// Four characters fit under the window, so the 40ms tick applies as-is.
|
||||
const chars = delayedChars()
|
||||
expect(chars).toHaveLength(4)
|
||||
expect(chars[1]!.style.animationDelay).toBe('190ms')
|
||||
expect(chars[3]!.style.animationDelay).toBe('270ms')
|
||||
})
|
||||
|
||||
it('starts a one-character name with no stagger at all', () => {
|
||||
vi.stubGlobal('matchMedia', vi.fn(() => ({ matches: false })))
|
||||
vi.useFakeTimers()
|
||||
const actions = renderSeat({
|
||||
current: 'creator',
|
||||
options: [{ id: 'creator', trust: 'user', name: 'C' }],
|
||||
introduce: true,
|
||||
})
|
||||
|
||||
expect(delayedChars()[0]!.style.animationDelay).toBe('150ms')
|
||||
act(() => { vi.advanceTimersByTime(550) })
|
||||
expect(actions.introduced).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('skips the run under reduced motion and acknowledges at once', () => {
|
||||
vi.stubGlobal('matchMedia', vi.fn(() => ({ matches: true })))
|
||||
const actions = renderSeat({ introduce: true })
|
||||
|
||||
expect(actions.introduced).toHaveBeenCalledTimes(1)
|
||||
expect(delayedChars()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('acknowledges an empty staged name without arming a run', () => {
|
||||
vi.stubGlobal('matchMedia', vi.fn(() => ({ matches: false })))
|
||||
const actions = renderSeat({
|
||||
current: 'creator',
|
||||
options: [{ id: 'creator', trust: 'user', name: '' }],
|
||||
introduce: true,
|
||||
})
|
||||
|
||||
expect(actions.introduced).toHaveBeenCalledTimes(1)
|
||||
expect(delayedChars()).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('the session-header label', () => {
|
||||
it('names the preset the session runs, and never offers a switch', async () => {
|
||||
const { load } = renderLabel({ blank: false, agentPreset: 'standard' })
|
||||
|
||||
@@ -253,6 +253,20 @@ describe('the preset list', () => {
|
||||
expect(actions.close).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps the empty custom group on screen: heading plus the creator entry', () => {
|
||||
renderSection({
|
||||
rows: [
|
||||
{ id: 'standard', trust: 'system', isDefault: true, name: '标准模式' },
|
||||
{ id: 'cordis', trust: 'system', isDefault: false, name: '创造模式' },
|
||||
],
|
||||
})
|
||||
|
||||
// No member yet, but the place where one's own preset will appear stays.
|
||||
expect(screen.getByRole('heading', { name: en.customGroup })).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: en.creatorDraft })).toBeTruthy()
|
||||
expect(screen.queryByText(`· ${en.userTrust}`)).toBeNull()
|
||||
})
|
||||
|
||||
it('hides the creator entry without the flow or the preset, disables it without a root', () => {
|
||||
renderSection()
|
||||
expect(screen.queryByRole('button', { name: en.creatorDraft })).toBeNull()
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-command",
|
||||
"description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-command"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -42,15 +49,15 @@
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-connection": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slash": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -64,7 +71,7 @@
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-conversation",
|
||||
"description": "Conversation domain: skeleton, ordered chat flow, composer with the Host-backed busy-Enter preference, and details host",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-conversation"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -41,25 +48,25 @@
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"clsx": "^2.0.0",
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-attachment": "^0.0.1",
|
||||
"@deepseek-ai/dsh-brand": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-connection": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-compact": "^0.0.1",
|
||||
"@deepseek-ai/dsh-commands": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm-retry": "^0.0.1",
|
||||
"@deepseek-ai/dsh-token-meter": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-attachment": "workspace:^",
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slash": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-compact": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-retry": "workspace:^",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -86,7 +93,7 @@
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-todo": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -242,6 +242,7 @@ function projectAssistant(context: ConversationNodeContext<AssistantState>): Ass
|
||||
/** Per-step Assistant streaming/final/interruption Definition. */
|
||||
export const assistantDefinition: ConversationNodeDefinition<AssistantState> = {
|
||||
kind: 'assistant-step',
|
||||
target: 'chat',
|
||||
match: (event) => {
|
||||
if (event.type === 'step/start') return { id: `${event.data.turn}:${event.data.step}`, role: 'start' }
|
||||
if (event.type === 'assistant/chunk'
|
||||
@@ -291,8 +292,7 @@ export const assistantDefinition: ConversationNodeDefinition<AssistantState> = {
|
||||
value: projected.data,
|
||||
}
|
||||
},
|
||||
buildViewNode: (context, target) => {
|
||||
if (target !== 'chat') return null
|
||||
buildViewNode: (context) => {
|
||||
const projected = projectAssistant(context)
|
||||
if (projected === undefined) return null
|
||||
if (projected.settled === undefined && !projected.visible) {
|
||||
|
||||
@@ -175,6 +175,7 @@ export function updateCompactionState<State extends CompactionEvidence>(
|
||||
/** Slash-command lifecycle, including integrated manual compaction, Definition. */
|
||||
export const commandDefinition: ConversationNodeDefinition<CommandState> = {
|
||||
kind: 'command',
|
||||
target: 'chat',
|
||||
match: (event) => {
|
||||
if (event.type === 'command/run') {
|
||||
return { id: String(event.data.commandId), role: 'start' }
|
||||
@@ -202,8 +203,7 @@ export const commandDefinition: ConversationNodeDefinition<CommandState> = {
|
||||
}
|
||||
return updateCompactionState(context.state, match)
|
||||
},
|
||||
buildViewNode: (context, target) => {
|
||||
if (target !== 'chat') return null
|
||||
buildViewNode: (context) => {
|
||||
const state = context.state ?? fallbackState(context)
|
||||
if (state === undefined) return null
|
||||
if (state.command.name !== 'compact') {
|
||||
|
||||
@@ -30,6 +30,7 @@ function fallbackState(context: ConversationNodeContext<CompactionState>): Compa
|
||||
/** Automatic compaction lifecycle and landed checkpoint Definition. */
|
||||
export const compactionDefinition: ConversationNodeDefinition<CompactionState> = {
|
||||
kind: 'compaction',
|
||||
target: 'chat',
|
||||
match: (event) => {
|
||||
const checkpoint = compactSource(event)
|
||||
if (checkpoint !== undefined && checkpoint.sourceCommandId === undefined) {
|
||||
@@ -47,8 +48,7 @@ export const compactionDefinition: ConversationNodeDefinition<CompactionState> =
|
||||
},
|
||||
start: () => ({}),
|
||||
update: (context, match) => updateCompactionState(context.state, match),
|
||||
buildViewNode: (context, target) => {
|
||||
if (target !== 'chat') return null
|
||||
buildViewNode: (context) => {
|
||||
const state = context.state ?? fallbackState(context)
|
||||
if (state.checkpoint === undefined) return null
|
||||
const marker = compactSummary(state.summary, state.checkpoint)
|
||||
|
||||
@@ -15,6 +15,7 @@ declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
|
||||
/** Unclaimed append-surface fallback Definition. */
|
||||
export const unknownFallbackDefinition: ConversationNodeDefinition<UnknownSurfaceNode> = {
|
||||
kind: 'unknown-surface',
|
||||
target: 'chat',
|
||||
match: event => isAppendSurfaceEvent(event)
|
||||
? { id: String(event.seq), role: 'start' }
|
||||
: null,
|
||||
@@ -26,7 +27,7 @@ export const unknownFallbackDefinition: ConversationNodeDefinition<UnknownSurfac
|
||||
data: match.event.data,
|
||||
}),
|
||||
update: context => context.state,
|
||||
buildViewNode: (context, target) => target !== 'chat' || context.state === undefined
|
||||
buildViewNode: context => context.state === undefined
|
||||
? null
|
||||
: chatNode(context, 'unknown', context.state.seq, context.state),
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ function inboxDefinition(target: InboxTarget): ConversationNodeDefinition<InboxS
|
||||
},
|
||||
update: context => context.state,
|
||||
publication: () => 'none',
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ function isCompactionCheckpoint(event: Parameters<ConversationNodeDefinition['ma
|
||||
/** User, steering, and injected-context message classification Definition. */
|
||||
export const messageDefinition: ConversationNodeDefinition<MessageNode> = {
|
||||
kind: 'input-message',
|
||||
target: 'chat',
|
||||
match: event => event.type === 'user/message'
|
||||
&& isAppendSurfaceEvent(event)
|
||||
&& !isCompactionCheckpoint(event)
|
||||
@@ -68,8 +69,8 @@ export const messageDefinition: ConversationNodeDefinition<MessageNode> = {
|
||||
}
|
||||
},
|
||||
update: context => context.state,
|
||||
buildViewNode: (context, target) => {
|
||||
if (target !== 'chat' || context.state === undefined) return null
|
||||
buildViewNode: (context) => {
|
||||
if (context.state === undefined) return null
|
||||
return chatNode(context, context.state.kind, context.state.seq, context.state)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ function isClosed(location: ConversationLocation): boolean {
|
||||
/** Producer-correlated model retry chain Definition. */
|
||||
export const retryDefinition: ConversationNodeDefinition<RetryState> = {
|
||||
kind: 'model-retry',
|
||||
target: 'chat',
|
||||
match: (event) => {
|
||||
if (event.type === 'llm/retry') {
|
||||
const retryId: unknown = event.data.retryId
|
||||
@@ -70,8 +71,8 @@ export const retryDefinition: ConversationNodeDefinition<RetryState> = {
|
||||
attempt.retry === retry ? { ...attempt, retryState: 'started' } : attempt),
|
||||
}
|
||||
},
|
||||
buildViewNode: (context, target) => {
|
||||
if (target !== 'chat' || context.state === undefined || context.state.attempts.length === 0) return null
|
||||
buildViewNode: (context) => {
|
||||
if (context.state === undefined || context.state.attempts.length === 0) return null
|
||||
const location = context.start?.location ?? context.matches[0]?.location ?? { kind: 'unresolved' as const }
|
||||
const stateAttempts = context.state.attempts
|
||||
const attempts = stateAttempts.map((attempt, index) =>
|
||||
|
||||
@@ -235,6 +235,7 @@ function fallbackState(context: ConversationNodeContext<ToolState>): ToolState |
|
||||
/** Root Tool lifecycle and nested Code Dispatch Definition. */
|
||||
export const toolDefinition: ConversationNodeDefinition<ToolState> = {
|
||||
kind: 'tool-call',
|
||||
target: 'chat',
|
||||
match: (event) => {
|
||||
if (event.type === 'tool/call') return { id: String(event.data.callId), role: 'start' }
|
||||
if (event.type === 'tool/result' && isAppendSurfaceEvent(event)) {
|
||||
@@ -257,8 +258,7 @@ export const toolDefinition: ConversationNodeDefinition<ToolState> = {
|
||||
}
|
||||
return updateDispatch(context.state, match)
|
||||
},
|
||||
buildViewNode: (context, target) => {
|
||||
if (target !== 'chat') return null
|
||||
buildViewNode: (context) => {
|
||||
const state = context.state ?? fallbackState(context)
|
||||
if (state === undefined) return null
|
||||
const projected = projectBlock(state.root, state, interruption(context))
|
||||
|
||||
@@ -63,6 +63,7 @@ function fallbackState(context: ConversationNodeContext<TurnErrorState>): TurnEr
|
||||
/** Terminal turn failure Definition, suppressed when the turn owns a retry chain. */
|
||||
export const turnErrorDefinition: ConversationNodeDefinition<TurnErrorState> = {
|
||||
kind: 'turn-error',
|
||||
target: 'chat',
|
||||
match: (event) => {
|
||||
if (event.type === 'turn/start') return { id: String(event.data.turn), role: 'start' }
|
||||
if (event.type === 'turn/end' && event.data.reason.kind === 'error') {
|
||||
@@ -82,8 +83,7 @@ export const turnErrorDefinition: ConversationNodeDefinition<TurnErrorState> = {
|
||||
? { ...context.state, hidden: true }
|
||||
: context.state
|
||||
},
|
||||
buildViewNode: (context, target) => {
|
||||
if (target !== 'chat') return null
|
||||
buildViewNode: (context) => {
|
||||
const state = context.state ?? fallbackState(context)
|
||||
if (state?.failure === undefined) return null
|
||||
const failure = state.failure
|
||||
|
||||
@@ -151,6 +151,7 @@ function tailData(context: ConversationNodeContext<TurnTailState>): TurnTailChat
|
||||
/** Completed-turn footer Definition independent of any Assistant row. */
|
||||
export const turnTailDefinition: ConversationNodeDefinition<TurnTailState> = {
|
||||
kind: 'turn-tail',
|
||||
target: 'chat',
|
||||
match: (event) => {
|
||||
if (event.type === 'turn/start') return { id: String(event.data.turn), role: 'start' }
|
||||
if (event.type === 'turn/end') return { id: String(event.data.turn), role: 'update' }
|
||||
@@ -179,8 +180,7 @@ export const turnTailDefinition: ConversationNodeDefinition<TurnTailState> = {
|
||||
value,
|
||||
}
|
||||
},
|
||||
buildViewNode: (context, target) => {
|
||||
if (target !== 'chat') return null
|
||||
buildViewNode: (context) => {
|
||||
const turn = turnLocation(context)
|
||||
const data = turn?.data.get('turn-tail')
|
||||
return data === undefined ? null : chatNode(context, 'turn-tail', closingAnchor(context), data)
|
||||
|
||||
@@ -60,7 +60,7 @@ export const zh = {
|
||||
'access.confirm.acknowledge': '我已了解风险,并愿意继续',
|
||||
'access.confirm.cancel': '取消',
|
||||
'access.confirm.enable': '启用 Full access',
|
||||
'hero.headline': '探索未知之境',
|
||||
'hero.headline': '探索未至之境',
|
||||
'hero.preview': '预览版',
|
||||
'hero.chooseWorkspace': '选择工作区',
|
||||
'session.hierarchy': '会话层级',
|
||||
|
||||
@@ -263,8 +263,9 @@
|
||||
.composerHero {
|
||||
position: relative; /* .heroGlow positioning context */
|
||||
align-self: center;
|
||||
/* figma 75:8208: 12 between hero chrome / workspace row / card. */
|
||||
gap: 12px;
|
||||
/* figma 75:8208 drew 12 between all three rows; the workspace row now sits
|
||||
8 above the card (its margin-top restores 12 under the hero chrome). */
|
||||
gap: 8px;
|
||||
/* Foot inside the centered box floats the stack a bit above true center. */
|
||||
padding-bottom: 32px;
|
||||
/* Card cap + both clearances: the hero input card lands at exactly the same
|
||||
@@ -292,7 +293,9 @@
|
||||
.heroWorkspaceRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
margin-top: 4px;
|
||||
/* figma drew px 8; nudged +12 so the chip's folder glyph lines up closer to
|
||||
the card's inner controls below. */
|
||||
padding-left: 20px;
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
min-height: 28px;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
border-radius: 16px;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font-size: 13px;
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
}
|
||||
|
||||
.chevron {
|
||||
/* inline-flex, not inline: an inline seat reserves baseline descent under
|
||||
the svg and floats the glyph off-center in the 28px trigger. */
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
color: var(--dsw-alias-label-caption);
|
||||
transition: transform 120ms ease;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import type {
|
||||
AssistantMessageNode, ConversationSnapshot, SessionId, ToolResultNode,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { EMPTY_CONVERSATION_VIEWS } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
|
||||
@@ -43,7 +44,7 @@ const assistant = (seq: number, turn: number, usage?: unknown): AssistantMessage
|
||||
|
||||
function snapshotBase(): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, chat: chatSnapshotFixture(),
|
||||
sessionId: SID, views: EMPTY_CONVERSATION_VIEWS, chat: chatSnapshotFixture(),
|
||||
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
|
||||
pending: [], queue: [], running: false, composerPhase: 'active', removed: false, openState: 'open', openError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, blank: false, subagent: null, lastAgentError: null,
|
||||
|
||||
@@ -12,7 +12,9 @@ import type {
|
||||
UserMessageNode, WorkspaceListState,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore, PendingWait } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
createSnapshotStore, EMPTY_CONVERSATION_VIEWS, PendingWait,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type {
|
||||
ChatNode, ChatNodeOwnerProps, ChatNodeViewProps, ChatViewSlotProps, SelectionTarget, UseChatNodeTurnData,
|
||||
@@ -47,7 +49,8 @@ type RoutedChatNodeOwner = ChatNodeOwnerProps & { readonly node: ChatNode }
|
||||
|
||||
function snapshotBase(): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, chat: chatSnapshotFixture(), nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
|
||||
sessionId: SID, views: EMPTY_CONVERSATION_VIEWS, chat: chatSnapshotFixture(), nodes: [],
|
||||
turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
|
||||
pending: [], queue: [], running: false, composerPhase: 'active', removed: false, openState: 'open', openError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, blank: false, subagent: null, lastAgentError: null,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore, EMPTY_CHAT_SNAPSHOT } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
createSnapshotStore, EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { UseSession } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { ConversationSnapshot, SessionId, SessionListState, WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SessionProviderComponent } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
@@ -48,7 +50,7 @@ function renderToolDetailsProbe(owners?: DetailsToolOwnerProps[]): DetailsSlotPr
|
||||
|
||||
function snapshotBase(): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
sessionId: SID, views: EMPTY_CONVERSATION_VIEWS, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
|
||||
pending: [], queue: [], running: false, composerPhase: 'active', removed: false, openState: 'open', openError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, blank: false, subagent: null, lastAgentError: null,
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore, EMPTY_CHAT_SNAPSHOT } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
createSnapshotStore, EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import type { ClientContext, ConversationSnapshot, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -37,7 +39,7 @@ const SID = 's1' as SessionId
|
||||
|
||||
function snapshotOf(overrides: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
sessionId: SID, views: EMPTY_CONVERSATION_VIEWS, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
|
||||
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
|
||||
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore, EMPTY_CHAT_SNAPSHOT } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
createSnapshotStore, EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ClientContext, ConversationSnapshot, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SubmitOutcome } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
@@ -26,7 +28,7 @@ const SID = 's1' as SessionId
|
||||
/** Standard-props InputBar mount over a real shell (the composer-bar entry shape). */
|
||||
function mountBar(shell: SessionInputShell, over?: { running?: boolean; disabled?: boolean }) {
|
||||
const session = createSnapshotStore<ConversationSnapshot>({
|
||||
sessionId: SID, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
sessionId: SID, views: EMPTY_CONVERSATION_VIEWS, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
|
||||
pending: [], queue: [], running: over?.running ?? false, composerPhase: 'active',
|
||||
removed: over?.disabled ?? false, openState: 'open', openError: null, hasMore: false,
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { EMPTY_CHAT_SNAPSHOT, SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS, SessionsService,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { SlashService } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import type { ClientSessionContext, CommandClaim, PickOutcome, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import { FakeApiClient, ok } from '../../runtime/tests/fake-api.ts'
|
||||
@@ -112,7 +114,7 @@ async function scopedBench(register?: (slash: SlashService) => void) {
|
||||
actx.on('slash/input-consume-token', req => shell.consumeToken(req.guard) ? true : undefined)
|
||||
const wiring = shell
|
||||
const sessionStore = createSnapshotStore<ConversationSnapshot>({
|
||||
sessionId, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
sessionId, views: EMPTY_CONVERSATION_VIEWS, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
|
||||
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
|
||||
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import { EMPTY_CHAT_SNAPSHOT } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
ConversationSnapshot, QueuedMessage, SessionId, SessionListState,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -33,7 +35,7 @@ function row(id: string, text: string | null, preview = text ?? '[image]'): Queu
|
||||
|
||||
function snapshotWith(queue: QueuedMessage[]): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
sessionId: SID, views: EMPTY_CONVERSATION_VIEWS, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
|
||||
pending: [], queue, running: true, composerPhase: 'active', removed: false, openState: 'open', openError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, blank: false, subagent: null, lastAgentError: null,
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createSnapshotStore, EMPTY_CHAT_SNAPSHOT } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
createSnapshotStore, EMPTY_CHAT_SNAPSHOT, EMPTY_CONVERSATION_VIEWS,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
ConversationSnapshot, SessionId, SessionListState, WorkspaceId, WorkspaceListState, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -70,7 +72,7 @@ const workspaceState = (items: readonly WorkspaceView[]): WorkspaceListState =>
|
||||
|
||||
function conversationSnapshot(overrides: Partial<ConversationSnapshot> = {}): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
sessionId: SID, views: EMPTY_CONVERSATION_VIEWS, chat: EMPTY_CHAT_SNAPSHOT,
|
||||
nodes: [], turnTimings: new Map(), turnEnds: new Map(), partial: null, runningCalls: [],
|
||||
pending: [], queue: [], running: false, composerPhase: 'active', removed: false,
|
||||
openState: 'open', openError: null, hasMore: false, loadingOlder: false,
|
||||
@@ -356,7 +358,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
const header = b.view.container.querySelector('header')
|
||||
expect(host).not.toBeNull()
|
||||
expect(header?.getAttribute('aria-hidden')).toBe('true')
|
||||
expect(b.view.getByText('探索未知之境')).toBeTruthy()
|
||||
expect(b.view.getByText('探索未至之境')).toBeTruthy()
|
||||
expect(b.view.getByText('预览版')).toBeTruthy()
|
||||
expect(b.view.queryByTestId('view-chat')).toBeNull()
|
||||
// The same machine-backed textarea is live in the hero, and the
|
||||
@@ -380,7 +382,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
const b = mount(conversationSnapshot({ composerPhase: 'blank', blank: true, openState: 'loading' }))
|
||||
const root = b.view.container.querySelector('[data-phase]')
|
||||
expect(root?.getAttribute('data-phase')).toBe('settling')
|
||||
expect(b.view.queryByText('探索未知之境')).toBeNull()
|
||||
expect(b.view.queryByText('探索未至之境')).toBeNull()
|
||||
})
|
||||
|
||||
it('settling phase: a session the list has no row for settles conservatively', () => {
|
||||
@@ -405,7 +407,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
// blank the column for the history round-trip.
|
||||
const root = b.view.container.querySelector('[data-phase]')
|
||||
expect(root?.getAttribute('data-phase')).toBe('hero')
|
||||
expect(b.view.getByText('探索未知之境')).toBeTruthy()
|
||||
expect(b.view.getByText('探索未至之境')).toBeTruthy()
|
||||
expect(b.view.getByRole('textbox')).toBeTruthy()
|
||||
})
|
||||
|
||||
@@ -423,7 +425,7 @@ describe('ConversationRoot resident composer', () => {
|
||||
expect(after.value).toBe('kept across flip')
|
||||
expect(b.chat.store.getSnapshot().draft).toBe('kept across flip')
|
||||
expect(b.view.container.querySelector('[data-conversation-scroll]')?.contains(after)).toBe(true)
|
||||
expect(b.view.queryByText('探索未知之境')).toBeNull()
|
||||
expect(b.view.queryByText('探索未至之境')).toBeNull()
|
||||
expect(b.view.getByTestId('view-chat')).toBeTruthy()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-deliverables",
|
||||
"description": "Produced-files turn tail: the deliverables row a finished turn ends with",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-deliverables"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -41,12 +48,12 @@
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
@@ -56,7 +63,7 @@
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -136,7 +136,6 @@ export const deliverablesDefinition: ConversationNodeDefinition<DeliverablesStat
|
||||
key: 'deliverables',
|
||||
value: { produced: context.state.produced },
|
||||
},
|
||||
buildViewNode: () => null,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,7 +73,7 @@ interface TimelineSnapshot {
|
||||
|
||||
class TestEventDefinitions {
|
||||
entries(): readonly ConversationNodeDefinition[] { return [deliverablesDefinition] }
|
||||
fallbackEntry(): undefined { return undefined }
|
||||
fallbackEntry(): ConversationNodeDefinition | undefined { return undefined }
|
||||
}
|
||||
|
||||
class TestViewDefinitions {
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-goal",
|
||||
"description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-goal"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -39,15 +46,15 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||
"@deepseek-ai/dsh-api-remotes": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-goal": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-api-remotes": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -62,7 +69,7 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-layout",
|
||||
"description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-layout"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -37,11 +44,11 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-theme": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-theme": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -51,7 +58,7 @@
|
||||
"@deepseek-ai/dsh-client-ui-theme": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-model",
|
||||
"description": "Model selection: the /model popupSelect over session.models / session.selectModel",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-model"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -38,17 +45,17 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-connection": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-command": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-command": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slash": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"clsx": "^2.1.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -63,7 +70,7 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"clsx": "^2.1.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-models",
|
||||
"description": "Models settings and official-DeepSeek first-run routing over one live provider/settings/credential join",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-models"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -38,14 +45,14 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-connection": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-schema-form": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-web-react": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-schema-form": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -60,7 +67,7 @@
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-permission",
|
||||
"description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-permission"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -39,17 +46,17 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-connection": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-schema-form": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-command": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slash": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-permission": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-schema-form": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-command": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slash": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-permission": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -65,7 +72,7 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-permission": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-plan",
|
||||
"description": "Plan-mode composer control: the conversation.input.plan seat over the plan projection and the /plan command channel",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-plan"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -38,15 +45,15 @@
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-connection": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-plan-mode": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-plan-mode": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -61,7 +68,7 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-plan-mode": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-primitives",
|
||||
"description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-primitives"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -45,7 +52,7 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@types/react-dom": "~18.3.0",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
@@ -53,7 +60,7 @@
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ interface AnchorProps {
|
||||
onBlur?: FocusEventHandler | undefined
|
||||
}
|
||||
|
||||
type TooltipLabel = string | (() => string)
|
||||
|
||||
/**
|
||||
* Attach a hover/focus tooltip to an anchor element.
|
||||
* @param props.label - bubble text.
|
||||
* @param props.label - bubble text, or a resolver evaluated only while the bubble is visible.
|
||||
* @param props.side - placement relative to the anchor (default 'right').
|
||||
* @param props.delayMs - hover delay in milliseconds; keyboard focus remains immediate.
|
||||
* @param props.disabled - suppress the bubble while true; the anchor renders identically so
|
||||
@@ -34,7 +36,7 @@ interface AnchorProps {
|
||||
* @param props.children - a single anchor element; its own ref (callback or object) is forwarded alongside the tooltip's.
|
||||
* @returns the cloned anchor plus a fixed-position bubble while hovered/focused.
|
||||
*/
|
||||
export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false, children }: { label: string; side?: TooltipSide; delayMs?: number; disabled?: boolean; children: ReactElement<AnchorProps> }) {
|
||||
export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false, children }: { label: TooltipLabel; side?: TooltipSide; delayMs?: number; disabled?: boolean; children: ReactElement<AnchorProps> }) {
|
||||
const anchor = useRef<HTMLElement | null>(null)
|
||||
// React 18 keeps the element's ref outside props; forward it so wrapping an
|
||||
// anchor in Tooltip never silently severs the owner's ref.
|
||||
@@ -46,6 +48,9 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
}, [childRef])
|
||||
const [pos, setPos] = useState<{ x: number; y: number } | null>(null)
|
||||
const bubble = useRef<HTMLSpanElement | null>(null)
|
||||
const resolvedLabel = pos === null
|
||||
? null
|
||||
: typeof label === 'function' ? label() : label
|
||||
// Horizontal viewport clamp: fixed positioning knows nothing about edges, so
|
||||
// a centered bubble near the right edge would clip. Each measurement resets
|
||||
// the base position before applying a direct style offset, allowing a shorter
|
||||
@@ -67,7 +72,7 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
clamp()
|
||||
window.addEventListener('resize', clamp)
|
||||
return () => { window.removeEventListener('resize', clamp) }
|
||||
}, [label, pos])
|
||||
}, [pos, resolvedLabel])
|
||||
const showTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
// Hover and focus are independent triggers: the bubble hides only after
|
||||
// BOTH clear (hovering away from a focused anchor must not drop it).
|
||||
@@ -128,7 +133,7 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
})}
|
||||
{pos !== null && (
|
||||
<span ref={bubble} className={css.bubble} data-side={side} style={{ left: pos.x, top: pos.y }} role="tooltip">
|
||||
{label}
|
||||
{resolvedLabel}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -349,6 +349,35 @@ export const IconThinkOutline16 = ({ size = 16, className }: IconProps) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** ic_ds_agent_preset_outline_16 (figma extract): node interiors knock out to transparency via mask, so the glyph sits on any fill. */
|
||||
export const IconAgentPresetOutline16 = ({ size = 16, className }: IconProps) => (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_agent_preset_16" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
<circle cx="7.9995" cy="3.28319" r="1.712" fill="black" />
|
||||
<circle cx="3.51122" cy="11.3855" r="1.712" fill="black" />
|
||||
<circle cx="12.4878" cy="11.3855" r="1.712" fill="black" />
|
||||
</mask>
|
||||
<path
|
||||
mask="url(#mask0_agent_preset_16)"
|
||||
d="M12.2881 11.0425C12.6002 11.3723 13.0413 11.5786 13.5312 11.5786L13.5342 11.5776C13.1476 12.3233 12.6119 12.9785 11.9639 13.5005C10.9327 14.3309 9.6199 14.8286 8.19336 14.8286C7.29864 14.8285 6.45056 14.6313 5.6875 14.2808C6.08309 14.0281 6.36707 13.6189 6.45215 13.1392C6.99022 13.3561 7.57767 13.476 8.19336 13.4761C9.30019 13.4761 10.3157 13.0915 11.1152 12.4478C11.5935 12.0626 11.9924 11.5848 12.2881 11.0425ZM4.14746 4.36475C4.25569 4.83228 4.55488 5.2247 4.95898 5.4585C4.07956 6.30639 3.53144 7.49605 3.53125 8.81396C3.53125 9.69534 3.77613 10.5202 4.20117 11.2231C3.74959 11.3817 3.38395 11.7232 3.19531 12.1597C2.5541 11.2032 2.17969 10.052 2.17969 8.81396C2.17989 7.05087 2.93868 5.4646 4.14746 4.36475ZM8.19336 2.80029C8.85717 2.80029 9.49784 2.90834 10.0967 3.10791C12.3237 3.85044 13.9725 5.86061 14.1846 8.28369C13.9832 8.20048 13.7627 8.15382 13.5312 8.15381C13.2802 8.15381 13.042 8.20907 12.8271 8.30615C12.6281 6.47264 11.3666 4.95616 9.66895 4.39014C9.2063 4.236 8.70989 4.15186 8.19336 4.15186C7.96112 4.15189 7.7329 4.16981 7.50977 4.20264C7.51947 4.12886 7.52637 4.05348 7.52637 3.97705C7.52628 3.56604 7.3811 3.18914 7.13965 2.89404C7.48183 2.83352 7.83381 2.80033 8.19336 2.80029Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M9.1123 3.28271C9.11205 2.66858 8.61322 2.17041 7.99902 2.17041C7.38504 2.17067 6.88697 2.66874 6.88672 3.28271C6.88672 3.89691 7.38489 4.39574 7.99902 4.396C8.61338 4.396 9.1123 3.89707 9.1123 3.28271ZM10.3115 3.28271C10.3115 4.55981 9.27612 5.59521 7.99902 5.59521C6.72214 5.59496 5.6875 4.55965 5.6875 3.28271C5.68776 2.00599 6.7223 0.971447 7.99902 0.971191C9.27596 0.971191 10.3113 2.00584 10.3115 3.28271Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M4.62402 11.385C4.62377 10.7709 4.12494 10.2727 3.51074 10.2727C2.89676 10.273 2.39869 10.771 2.39844 11.385C2.39844 11.9992 2.89661 12.498 3.51074 12.4983C4.1251 12.4983 4.62402 11.9994 4.62402 11.385ZM5.82324 11.385C5.82324 12.6621 4.78784 13.6975 3.51074 13.6975C2.23386 13.6973 1.19922 12.6619 1.19922 11.385C1.19947 10.1083 2.23402 9.07374 3.51074 9.07349C4.78768 9.07349 5.82299 10.1081 5.82324 11.385Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M13.6006 11.385C13.6003 10.7709 13.1015 10.2727 12.4873 10.2727C11.8733 10.273 11.3753 10.771 11.375 11.385C11.375 11.9992 11.8732 12.498 12.4873 12.4983C13.1017 12.4983 13.6006 11.9994 13.6006 11.385ZM14.7998 11.385C14.7998 12.6621 13.7644 13.6975 12.4873 13.6975C11.2104 13.6973 10.1758 12.6619 10.1758 11.385C10.176 10.1083 11.2106 9.07374 12.4873 9.07349C13.7642 9.07349 14.7995 10.1081 14.7998 11.385Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** ic_ds_browse_outline_16 */
|
||||
export const IconBrowseOutline16 = ({ size = 16, className }: IconProps) => (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -16,8 +16,8 @@ const icons = Object.fromEntries(
|
||||
const iconNames = Object.keys(icons)
|
||||
|
||||
describe('ic_ds_ icon set', () => {
|
||||
it('exports the full icon set (46 deepsuite + 18 figma extracts + three product glyphs outside those sets)', () => {
|
||||
expect(iconNames.length).toBe(67)
|
||||
it('exports the full icon set (46 deepsuite + 19 figma extracts + three product glyphs outside those sets)', () => {
|
||||
expect(iconNames.length).toBe(68)
|
||||
})
|
||||
|
||||
it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', (name) => {
|
||||
|
||||
@@ -6,6 +6,27 @@ import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('Tooltip', () => {
|
||||
it('resolves lazy labels only after the bubble becomes visible', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const label = vi.fn(() => 'Timing details')
|
||||
render(
|
||||
<Tooltip label={label} delayMs={500}>
|
||||
<button type="button">anchor</button>
|
||||
</Tooltip>,
|
||||
)
|
||||
expect(label).not.toHaveBeenCalled()
|
||||
fireEvent.mouseEnter(screen.getByText('anchor'))
|
||||
act(() => { vi.advanceTimersByTime(499) })
|
||||
expect(label).not.toHaveBeenCalled()
|
||||
act(() => { vi.advanceTimersByTime(1) })
|
||||
expect(screen.getByRole('tooltip').textContent).toBe('Timing details')
|
||||
expect(label).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('can delay pointer hover without delaying keyboard focus', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-question",
|
||||
"description": "Web ask_user_question feature: host tool mount plus composer-takeover question UI",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-question"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -46,9 +53,9 @@
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
@@ -58,7 +65,7 @@
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-settings-general",
|
||||
"description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-settings-general"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -40,18 +47,18 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-settings": "workspace:^",
|
||||
"@deepseek-ai/schemastery": "^3.18.0"
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-connection": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-locale": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-web-react": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -65,7 +72,7 @@
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@deepseek-ai/cordis": "^4.0.0-rc.7",
|
||||
"@deepseek-ai/cordis": "workspace:^",
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user