Merge remote-tracking branch 'origin/master' into worktree/pr823-retarget-latest-20260729

# Conflicts:
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/skills.i18n.yaml
#	docs/core-data-structures/skills.md
#	docs/core-data-structures/skills.zh.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/skill/skill-local/README.i18n.yaml
#	packages/skill/skill/README.i18n.yaml
#	packages/skill/skill/README.md
#	packages/skill/skill/README.zh.md
#	packages/skill/skill/src/index.ts
#	packages/skill/skill/tests/skill.spec.ts
#	packages/skill/tool-skill/README.i18n.yaml
#	packages/skill/tool-skill/src/index.ts
#	packages/ui/tui/README.i18n.yaml
#	packages/ui/tui/README.md
#	packages/ui/tui/README.zh.md
#	packages/ui/tui/src/index.ts
#	packages/ui/tui/tests/tui.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-29 23:36:49 +08:00
205 changed files with 5429 additions and 840 deletions

View File

@@ -1,6 +1,7 @@
import { mkdirSync, writeFileSync } from 'node:fs'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { dirname, join } from 'node:path'
import { execa } from 'execa'
import { resolveExampleLaunch, type ExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
@@ -36,7 +37,16 @@ while time.monotonic() < deadline:
if chunk:
output.extend(chunk)
while action_index < len(actions) and actions[action_index]["waitFor"].encode() in output:
os.write(fd, actions[action_index]["send"].encode())
action = actions[action_index]
if "writeFile" in action:
target = os.path.join(cwd, action["writeFile"]["path"])
os.makedirs(os.path.dirname(target), exist_ok=True)
with open(target, "w", encoding="utf-8") as handle:
handle.write(action["writeFile"]["content"])
if "send" in action:
os.write(fd, action["send"].encode())
else:
os.write(fd, action["send"].encode())
action_index += 1
waited, candidate = os.waitpid(pid, os.WNOHANG)
if waited == pid:
@@ -56,11 +66,14 @@ if actual_exit != int(expected_exit):
sys.exit(125)
`
/** One terminal action sent after its marker has rendered. */
interface TuiPtyAction {
readonly waitFor: string
readonly send: string
}
/** One terminal input or workspace mutation performed after its marker renders. */
type TuiPtyAction =
| { readonly waitFor: string; readonly send: string }
| {
readonly waitFor: string
readonly writeFile: { readonly path: string; readonly content: string }
readonly send?: string
}
/** Inputs for a keyless real-Loader TUI process smoke. */
export interface TuiPtySmokeOptions {
@@ -157,7 +170,16 @@ async function runWindowsPtySmoke(
terminal.onData((chunk) => {
output += chunk
while (actionIndex < actions.length && output.includes(actions[actionIndex]!.waitFor)) {
terminal.write(actions[actionIndex]!.send)
const action = actions[actionIndex]!
if ('writeFile' in action) {
const target = join(cwd, action.writeFile.path)
mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, action.writeFile.content)
const input = action.send
if (input !== undefined) terminal.write(input)
} else {
terminal.write(action.send)
}
actionIndex += 1
}
})

View File

@@ -252,6 +252,36 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('adds a watched local skill to live /skill: autocomplete without restarting', async () => {
const skill = [
'---',
'name: hot-added-skill',
'description: HOT_ADDED_COMPLETION_MARKER',
'---',
'',
'Hot-added body.',
'',
].join('\n')
const output = await smoke({
label: 'tui-agent hot-added skill autocomplete',
tempDirPrefix: 'tui-agent-hot-skill-',
configPath: scriptedConfigPath,
actions: [
{
waitFor: 'scripted TUI ready.',
writeFile: {
path: '.agents/skills/hot-added-skill/SKILL.md',
content: skill,
},
send: '/skill:hot',
},
{ waitFor: 'HOT_ADDED_COMPLETION_MARKER', send: '\x03/exit\r' },
],
})
expect(output).toContain('HOT_ADDED_COMPLETION_MARKER')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
it('fuzzy-completes an @file path without reading or submitting the file', async () => {
const output = await smoke({
label: 'tui-agent file autocomplete',

View File

@@ -336,8 +336,9 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
terminal.send('\x03')
await settleTerminal(terminal)
const skillContext = ctx
const skillTurnEnded = new Promise<void>((resolve) => {
const detach = ctx.on('session/event', (session, event) => {
const detach = skillContext.on('session/event', (session, event) => {
if (session !== agent.session || event.type !== 'turn/end') return
detach()
resolve()