@x-otto/cli 0.0.1-alpha.49 → 0.0.1-alpha.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/{auth-command-D1T16ed-.js → auth-command-BYaFM76w.js} +2 -2
  2. package/dist/{auth-command-D1T16ed-.js.map → auth-command-BYaFM76w.js.map} +1 -1
  3. package/dist/backend-handler-DUafuNWw.js +3 -0
  4. package/dist/backend-handler-DUafuNWw.js.map +1 -0
  5. package/dist/build-id-DjE6eVpQ.js +3 -0
  6. package/dist/build-id-DjE6eVpQ.js.map +1 -0
  7. package/dist/bundled-extensions/plugin-feedback/otto-plugin.json +24 -0
  8. package/dist/bundled-extensions/plugin-feedback/panels/feedback.tsx +348 -0
  9. package/dist/bundled-extensions/plugin-feedback/panels/panel-host-globals.d.ts +67 -0
  10. package/dist/bundled-extensions/plugin-feedback/panels-dist/feedback.mjs +257 -0
  11. package/dist/bundled-extensions/plugin-feedback/plugin-dist/meta.json +1 -0
  12. package/dist/bundled-extensions/plugin-feedback/plugin-dist/plugin.cjs +11304 -0
  13. package/dist/bundled-extensions/plugin-feedback/plugin.ts +198 -0
  14. package/dist/bundled-extensions/plugin-feedback/src/coding-adapter.ts +183 -0
  15. package/dist/bundled-extensions/plugin-feedback/src/coding-lister.ts +26 -0
  16. package/dist/bundled-extensions/plugin-feedback/src/github-lister.ts +230 -0
  17. package/dist/bundled-extensions/plugin-feedback/src/issue-aggregator.ts +109 -0
  18. package/dist/bundled-extensions/plugin-feedback/src/issue-submitter.ts +241 -0
  19. package/dist/bundled-extensions/plugin-feedback/src/platform-resolver.ts +76 -0
  20. package/dist/bundled-extensions/plugin-feedback/src/pulse-survey-sink.ts +114 -0
  21. package/dist/bundled-extensions/plugin-feedback/src/resolved-sink.ts +163 -0
  22. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/meta.json +1 -1
  23. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/plugin.cjs +605 -725
  24. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/anthropic-messages.ts +4 -22
  25. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-completions.ts +5 -5
  26. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-responses.ts +3 -15
  27. package/dist/command-dispatcher-BLT8IO4K.js +18 -0
  28. package/dist/command-dispatcher-BLT8IO4K.js.map +1 -0
  29. package/dist/create-host-app-CbJ8ptv-.js +3 -0
  30. package/dist/{create-host-app-BXQ8bU-B.js.map → create-host-app-CbJ8ptv-.js.map} +1 -1
  31. package/dist/{daemon-base-Y1XI61JY.js → daemon-base-D015ew5y.js} +1 -1
  32. package/dist/{daemon-base-Y1XI61JY.js.map → daemon-base-D015ew5y.js.map} +1 -1
  33. package/dist/{doctor-DXBLkbkP.js → doctor-EndB-ycb.js} +3 -3
  34. package/dist/{doctor-DXBLkbkP.js.map → doctor-EndB-ycb.js.map} +1 -1
  35. package/dist/{doctor-attach-DEMuyBOk.js → doctor-attach-DEoE7eFx.js} +2 -2
  36. package/dist/{doctor-attach-DEMuyBOk.js.map → doctor-attach-DEoE7eFx.js.map} +1 -1
  37. package/dist/{doctor-cdp-CnJUVNOB.js → doctor-cdp-Cj9IFpEP.js} +1 -1
  38. package/dist/{doctor-cdp-CnJUVNOB.js.map → doctor-cdp-Cj9IFpEP.js.map} +1 -1
  39. package/dist/{doctor-heap-Eu3LZj-F.js → doctor-heap-SIxiRvL8.js} +2 -2
  40. package/dist/{doctor-heap-Eu3LZj-F.js.map → doctor-heap-SIxiRvL8.js.map} +1 -1
  41. package/dist/{doctor-ps-JBgCH77n.js → doctor-ps-DNVOF6yY.js} +2 -2
  42. package/dist/{doctor-ps-JBgCH77n.js.map → doctor-ps-DNVOF6yY.js.map} +1 -1
  43. package/dist/extension-CoJqqktf.js +2 -0
  44. package/dist/extension-Dpn8gltG.js +179 -0
  45. package/dist/extension-Dpn8gltG.js.map +1 -0
  46. package/dist/feedback-QcONRnO3.js +30 -0
  47. package/dist/feedback-QcONRnO3.js.map +1 -0
  48. package/dist/feedback-backend-methods-BNm38ddr.js +3 -0
  49. package/dist/feedback-backend-methods-BNm38ddr.js.map +1 -0
  50. package/dist/{feishu-setup-5ZoxtH0J.js → feishu-setup-DbAq_BRH.js} +1 -1
  51. package/dist/feishu-setup-DbAq_BRH.js.map +1 -0
  52. package/dist/{ftue-doctor-recovery-8FeDBFOc.js → ftue-doctor-recovery-CKTMnxM8.js} +1 -1
  53. package/dist/{ftue-doctor-recovery-8FeDBFOc.js.map → ftue-doctor-recovery-CKTMnxM8.js.map} +1 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +11 -11
  56. package/dist/index.js.map +1 -1
  57. package/dist/install-CEPczuet.js +11 -0
  58. package/dist/install-CEPczuet.js.map +1 -0
  59. package/dist/install-CG6b6fox.js +2 -0
  60. package/dist/interactive-schedule-Bv__2Kwc.js +3 -0
  61. package/dist/interactive-schedule-Bv__2Kwc.js.map +1 -0
  62. package/dist/{mcp-Cm4-OhXI.js → mcp-DjReRjuu.js} +1 -1
  63. package/dist/{mcp-Cm4-OhXI.js.map → mcp-DjReRjuu.js.map} +1 -1
  64. package/dist/{mcp-server-Dpza3yn2.js → mcp-server-Rr__aWO7.js} +3 -3
  65. package/dist/mcp-server-Rr__aWO7.js.map +1 -0
  66. package/dist/{migrate-RlBJqJnB.js → migrate-BBi3Txu2.js} +1 -1
  67. package/dist/{migrate-RlBJqJnB.js.map → migrate-BBi3Txu2.js.map} +1 -1
  68. package/dist/{observe-Dxfud-t7.js → observe-BqLCpmFD.js} +2 -2
  69. package/dist/{observe-Dxfud-t7.js.map → observe-BqLCpmFD.js.map} +1 -1
  70. package/dist/parse-cli-Db8pF_Ya.js +3 -0
  71. package/dist/parse-cli-Db8pF_Ya.js.map +1 -0
  72. package/dist/{persistence-CRFhOYx6.js → persistence-BvLRnpfi.js} +2 -2
  73. package/dist/{persistence-CRFhOYx6.js.map → persistence-BvLRnpfi.js.map} +1 -1
  74. package/dist/{plugin-assembly-check-DEozmcJ7.js → plugin-assembly-check-BntT5W6H.js} +1 -1
  75. package/dist/{plugin-assembly-check-DEozmcJ7.js.map → plugin-assembly-check-BntT5W6H.js.map} +1 -1
  76. package/dist/{proxy-evg7ZvoD.js → proxy-CFZDVIwI.js} +1 -1
  77. package/dist/proxy-CFZDVIwI.js.map +1 -0
  78. package/dist/proxy-CkSf8ziQ.js +2 -0
  79. package/dist/{ps-DiKCLKbs.js → ps-BVKv6rAo.js} +1 -1
  80. package/dist/{ps-DiKCLKbs.js.map → ps-BVKv6rAo.js.map} +1 -1
  81. package/dist/{registry-BdCcwyGy.js → registry-BUvu-es1.js} +4 -4
  82. package/dist/registry-BUvu-es1.js.map +1 -0
  83. package/dist/registry-DYHR-ONu.js +2 -0
  84. package/dist/{run-attach-DDpMk2_N.js → run-attach-X35puJ_n.js} +2 -2
  85. package/dist/{run-attach-DDpMk2_N.js.map → run-attach-X35puJ_n.js.map} +1 -1
  86. package/dist/schedule-Ke0jwatM.js +5 -0
  87. package/dist/schedule-Ke0jwatM.js.map +1 -0
  88. package/dist/serve-K6BOT5K_.js +21 -0
  89. package/dist/serve-K6BOT5K_.js.map +1 -0
  90. package/dist/{service-client-Cr4sxDVM.js → service-client-cfUUP4EE.js} +1 -1
  91. package/dist/{service-client-Cr4sxDVM.js.map → service-client-cfUUP4EE.js.map} +1 -1
  92. package/dist/{version-BrSIyY6s.js → version-DrCy32wR.js} +2 -2
  93. package/dist/{version-BrSIyY6s.js.map → version-DrCy32wR.js.map} +1 -1
  94. package/package.json +16 -13
  95. package/dist/build-id-DxfuzdUE.js +0 -3
  96. package/dist/build-id-DxfuzdUE.js.map +0 -1
  97. package/dist/command-dispatcher-DTOPjtZP.js +0 -21
  98. package/dist/command-dispatcher-DTOPjtZP.js.map +0 -1
  99. package/dist/create-host-app-BXQ8bU-B.js +0 -3
  100. package/dist/create-host-app-DG58-vRA.js +0 -2
  101. package/dist/extension-CTyrcdT1.js +0 -2
  102. package/dist/extension-DVEml5IN.js +0 -179
  103. package/dist/extension-DVEml5IN.js.map +0 -1
  104. package/dist/feedback-DvWUvSc4.js +0 -21
  105. package/dist/feedback-DvWUvSc4.js.map +0 -1
  106. package/dist/feedback-DzPLfcfN.js +0 -2
  107. package/dist/feishu-setup-5ZoxtH0J.js.map +0 -1
  108. package/dist/install-DlVJRHRT.js +0 -11
  109. package/dist/install-DlVJRHRT.js.map +0 -1
  110. package/dist/install-WCcQ9k9M.js +0 -2
  111. package/dist/mcp-server-Dpza3yn2.js.map +0 -1
  112. package/dist/parse-cli-BvcxTK88.js +0 -3
  113. package/dist/parse-cli-BvcxTK88.js.map +0 -1
  114. package/dist/proxy-BvdBsB1O.js +0 -2
  115. package/dist/proxy-evg7ZvoD.js.map +0 -1
  116. package/dist/registry-BOX7DaxS.js +0 -2
  117. package/dist/registry-BdCcwyGy.js.map +0 -1
  118. package/dist/serve-Ke0nwN9Y.js +0 -21
  119. package/dist/serve-Ke0nwN9Y.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{t as e}from"./create-host-app-BXQ8bU-B.js";import{createDefaultAuthStore as t}from"@x-otto/ai";import{createInterface as n}from"node:readline/promises";async function r(n,i={}){if(!i.authStore&&i.bootstrap){let t=e({workspaceDir:i.bootstrap.workspaceDir,projectConfigPath:i.bootstrap.projectConfigPath,logger:{name:`otto-auth`,level:i.bootstrap.verbose?`debug`:`info`,destination:`stderr`}});try{return await t.start(),await r(n,{...i,authStore:t.authStore,bootstrap:void 0})}finally{await t.stop()}}let s=i.stdout??(e=>process.stdout.write(e)),l=i.stderr??(e=>process.stderr.write(e)),u=i.authStore??t({watchForExternalChanges:!1}),[d,f]=n;switch(d){case`login`:return a(u,f,{stdout:s,stderr:l,prompt:i.prompt});case`logout`:return o(u,f,{stdout:s,stderr:l});case`status`:case void 0:return c(u,{stdout:s});default:return l(`otto auth: unknown action '${d}'. Use login | status | logout.\n`),1}}function i(e){return e.oauth.list().map(e=>e.id)}async function a(e,t,r){let a=i(e);if(a.length===0)return r.stderr(`otto auth login: no OAuth-capable providers available yet.
2
+ import{t as e}from"./create-host-app-CbJ8ptv-.js";import{createDefaultAuthStore as t}from"@x-otto/ai";import{createInterface as n}from"node:readline/promises";async function r(n,i={}){if(!i.authStore&&i.bootstrap){let t=e({workspaceDir:i.bootstrap.workspaceDir,projectConfigPath:i.bootstrap.projectConfigPath,logger:{name:`otto-auth`,level:i.bootstrap.verbose?`debug`:`info`,destination:`stderr`}});try{return await t.start(),await r(n,{...i,authStore:t.authStore,bootstrap:void 0})}finally{await t.stop()}}let s=i.stdout??(e=>process.stdout.write(e)),l=i.stderr??(e=>process.stderr.write(e)),u=i.authStore??t({watchForExternalChanges:!1}),[d,f]=n;switch(d){case`login`:return a(u,f,{stdout:s,stderr:l,prompt:i.prompt});case`logout`:return o(u,f,{stdout:s,stderr:l});case`status`:case void 0:return c(u,{stdout:s});default:return l(`otto auth: unknown action '${d}'. Use login | status | logout.\n`),1}}function i(e){return e.oauth.list().map(e=>e.id)}async function a(e,t,r){let a=i(e);if(a.length===0)return r.stderr(`otto auth login: no OAuth-capable providers available yet.
3
3
  Install a provider plugin first — run \`otto\` (interactive) for guided setup,
4
4
  or: otto extension install anthropic-provider
5
5
  `),1;if(!t)return r.stderr(`otto auth login: missing provider. One of: ${a.join(`, `)}\n`),1;let o=e.oauth.get(t);if(!o)return r.stderr(`otto auth login: unknown provider '${t}'. One of: ${a.join(`, `)}\n`),r.stderr(`Hint: plugin providers (cursor, anthropic, …) need the plugin installed with provider capability granted.
@@ -8,4 +8,4 @@ or: otto extension install anthropic-provider
8
8
  `),1;let s=r.prompt?void 0:n({input:process.stdin,output:process.stdout}),c=r.prompt??(e=>s.question(e)),l=new AbortController,u=()=>l.abort();process.on(`SIGINT`,u);try{let n={onAuth:e=>{r.stdout(`\nOpen this URL in your browser to authorize:\n ${e.url}\n`),e.code&&r.stdout(`Code: ${e.code}\n`),e.instructions&&r.stdout(`${e.instructions}\n`)},onPrompt:async e=>{let t=e.placeholder?` (${e.placeholder})`:``;return(await c(`${e.message}${t}: `)).trim()},onManualCode:async e=>{let t=c(`
9
9
  Paste the code here: `).then(e=>e.trim(),()=>``);return e?e.aborted?``:Promise.race([t,new Promise(t=>{e.addEventListener(`abort`,()=>t(``),{once:!0})})]):t},onProgress:e=>r.stdout(`${e}\n`),signal:l.signal};return await e.login(t,n),r.stdout(`\n✓ Logged in to ${o.name}.\n`),0}catch(e){return r.stderr(`\notto auth login failed: ${e.message}\n`),1}finally{process.off(`SIGINT`,u),s?.close()}}async function o(e,t,n){if(!t)return n.stderr(`otto auth logout: missing provider. One of: ${e.listProviders().join(`, `)}\n`),1;let r=await e.hasCredential(t);return await e.logout(t),n.stdout(r?`✓ Logged out of ${t}.\n`:`${t} was not logged in.\n`),0}const s={oauth:`OAuth`,api_key:`API key (stored)`,env:`API key (env)`};async function c(e,t){await e.ensureInitialized(),t.stdout(`Authentication status:
10
10
  `);for(let n of e.listProviders()){let r=await e.getAuthSource(n),i=r?s[r]:`not configured`;t.stdout(` ${n.padEnd(16)} ${i}\n`)}return 0}export{r as runAuthCommand};
11
- //# sourceMappingURL=auth-command-D1T16ed-.js.map
11
+ //# sourceMappingURL=auth-command-BYaFM76w.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-command-D1T16ed-.js","names":[],"sources":["../src/auth-command.ts"],"sourcesContent":["import { createInterface } from 'node:readline/promises'\n\nimport { createHostApp } from './create-host-app'\nimport { createDefaultAuthStore } from '@x-otto/ai'\nimport type { AuthStore, OAuthLoginOptions } from '@x-otto/ai'\n\n/**\n * `otto auth login|status|logout` 子命令(M8-07)。\n *\n * 驱动 AuthStore.login 的 onAuth/onPrompt/onManualCode 回调:\n * - Anthropic(usesCallbackServer=false)→ loopback(54545) 优先自动回填,\n * 端口占用时回退托管回调 + 用户回贴 code(onManualCode 必须提供);\n * - loopback provider(OpenAI/Codex,usesCallbackServer=true)→ 浏览器重定向自动完成。\n * - 插件 OAuth(cursor/anthropic 等)→ 须 bootstrap App 装载插件后再查 oauth 表(RFC-130 M131-1)。\n */\nexport interface AuthBootstrapOptions {\n workspaceDir: string\n projectConfigPath?: string\n verbose?: boolean\n}\n\nexport interface AuthCommandDeps {\n authStore?: AuthStore\n /** 未注入 authStore 时,经 App.start() 装载插件 OAuth(对齐 TUI `/auth`)。 */\n bootstrap?: AuthBootstrapOptions\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n /** 单行读取器(注入便于测试);未提供时按需创建 readline。 */\n prompt?: (question: string) => Promise<string>\n}\n\nexport async function runAuthCommand(args: string[], deps: AuthCommandDeps = {}): Promise<number> {\n if (!deps.authStore && deps.bootstrap) {\n const app = createHostApp({\n workspaceDir: deps.bootstrap.workspaceDir,\n projectConfigPath: deps.bootstrap.projectConfigPath,\n logger: {\n name: 'otto-auth',\n level: deps.bootstrap.verbose ? 'debug' : 'info',\n destination: 'stderr',\n },\n })\n try {\n await app.start()\n return await runAuthCommand(args, {\n ...deps,\n authStore: app.authStore,\n bootstrap: undefined,\n })\n } finally {\n await app.stop()\n }\n }\n\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n // watchForExternalChanges:false —— `otto auth *` 是一次性命令,跑完即退出,不需要跨进程凭据\n // 实时同步(那是给 TUI/serve 长驻进程用的)。AuthStore 的 fs.watch 句柄本身已 unref(不阻塞\n // 事件循环清空,见 auth-store.ts setupWatcher 注释),此处显式关闭仍是更准确的语义表达——\n // 避免为一个秒退的脚本无谓建立/拆除一个不会被用到的 watcher。\n const authStore = deps.authStore ?? createDefaultAuthStore({ watchForExternalChanges: false })\n\n const [action, provider] = args\n\n switch (action) {\n case 'login':\n return authLogin(authStore, provider, { stdout, stderr, prompt: deps.prompt })\n case 'logout':\n return authLogout(authStore, provider, { stdout, stderr })\n case 'status':\n case undefined:\n return authStatus(authStore, { stdout })\n default:\n stderr(`otto auth: unknown action '${action}'. Use login | status | logout.\\n`)\n return 1\n }\n}\n\nfunction loginableProviders(authStore: AuthStore): string[] {\n return authStore.oauth.list().map((p) => p.id)\n}\n\nasync function authLogin(\n authStore: AuthStore,\n provider: string | undefined,\n io: { stdout: (s: string) => void; stderr: (s: string) => void; prompt?: (q: string) => Promise<string> },\n): Promise<number> {\n const providers = loginableProviders(authStore)\n // UX(round 6):零 provider 插件时 providers 为空数组——\"One of: \"后面是空串,\n // 用户面对死端。指回引导路径而不是展示空名单。\n if (providers.length === 0) {\n io.stderr(\n 'otto auth login: no OAuth-capable providers available yet.\\n' +\n 'Install a provider plugin first — run `otto` (interactive) for guided setup,\\n' +\n 'or: otto extension install anthropic-provider\\n',\n )\n return 1\n }\n if (!provider) {\n io.stderr(`otto auth login: missing provider. One of: ${providers.join(', ')}\\n`)\n return 1\n }\n\n const oauth = authStore.oauth.get(provider)\n if (!oauth) {\n io.stderr(`otto auth login: unknown provider '${provider}'. One of: ${providers.join(', ')}\\n`)\n io.stderr(\n 'Hint: plugin providers (cursor, anthropic, …) need the plugin installed with provider capability granted.\\n' +\n ' otto extension install --link extensions/plugin-cursor --yes\\n' +\n ' # user-scope install auto-trusts; then otto auth login cursor\\n',\n )\n return 1\n }\n\n const rl = io.prompt ? undefined : createInterface({ input: process.stdin, output: process.stdout })\n const ask = io.prompt ?? ((q: string) => rl!.question(q))\n\n const controller = new AbortController()\n const onSigint = (): void => controller.abort()\n process.on('SIGINT', onSigint)\n\n try {\n const options: OAuthLoginOptions = {\n onAuth: (info) => {\n io.stdout(`\\nOpen this URL in your browser to authorize:\\n ${info.url}\\n`)\n if (info.code) {\n io.stdout(`Code: ${info.code}\\n`)\n }\n if (info.instructions) {\n io.stdout(`${info.instructions}\\n`)\n }\n },\n onPrompt: async (p) => {\n const suffix = p.placeholder ? ` (${p.placeholder})` : ''\n return (await ask(`${p.message}${suffix}: `)).trim()\n },\n // loopback 回调先完成时经 signal 放行。readline 本身无法中断,由 finally 的\n // rl.close() 收尾——close 会 reject 悬挂的 question,故 catch 归一为空串防 unhandled。\n onManualCode: async (signal?: AbortSignal) => {\n const question = ask('\\nPaste the code here: ').then(\n (s) => s.trim(),\n () => '',\n )\n if (!signal) return question\n if (signal.aborted) return ''\n return Promise.race([\n question,\n new Promise<string>((resolve) => {\n signal.addEventListener('abort', () => resolve(''), { once: true })\n }),\n ])\n },\n onProgress: (m) => io.stdout(`${m}\\n`),\n signal: controller.signal,\n }\n\n await authStore.login(provider, options)\n io.stdout(`\\n✓ Logged in to ${oauth.name}.\\n`)\n return 0\n } catch (error) {\n io.stderr(`\\notto auth login failed: ${(error as Error).message}\\n`)\n return 1\n } finally {\n process.off('SIGINT', onSigint)\n rl?.close()\n }\n}\n\nasync function authLogout(\n authStore: AuthStore,\n provider: string | undefined,\n io: { stdout: (s: string) => void; stderr: (s: string) => void },\n): Promise<number> {\n if (!provider) {\n io.stderr(\n `otto auth logout: missing provider. One of: ${authStore.listProviders().join(', ')}\\n`,\n )\n return 1\n }\n\n const had = await authStore.hasCredential(provider)\n await authStore.logout(provider)\n io.stdout(had ? `✓ Logged out of ${provider}.\\n` : `${provider} was not logged in.\\n`)\n return 0\n}\n\nconst SOURCE_LABEL: Record<string, string> = {\n oauth: 'OAuth',\n api_key: 'API key (stored)',\n env: 'API key (env)',\n}\n\nasync function authStatus(\n authStore: AuthStore,\n io: { stdout: (s: string) => void },\n): Promise<number> {\n // RFC-341 修复:`AuthStore.listProviders()` 是同步方法,读的是内存 cache——\n // cache 由 `ensureInitialized()`(异步读盘)填充,且没有任何调用方保证在\n // `listProviders()` 之前完成过初始化(`getAuthSource()` 等其余方法内部各自会\n // `await ensureInitialized()`,唯独 `listProviders()` 没有)。此前这里直接调\n // `listProviders()` 时 cache 大概率还是空的(首次调用、无其余方法先触发过初始化\n // 的一次性命令场景),导致 `otto auth status`/Setup Wizard 装完 provider 后\n // 立即查询状态永远显示\"未配置\"——即便凭据其实已经成功落盘(真实事故:OrbStack\n // 虚拟机全流程测试实测复现,auth.json 里确有凭据但 status/doctor 均报未配置)。\n await authStore.ensureInitialized()\n io.stdout('Authentication status:\\n')\n for (const provider of authStore.listProviders()) {\n const source = await authStore.getAuthSource(provider)\n const label = source ? SOURCE_LABEL[source] : 'not configured'\n io.stdout(` ${provider.padEnd(16)} ${label}\\n`)\n }\n return 0\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth-command-BYaFM76w.js","names":[],"sources":["../src/auth-command.ts"],"sourcesContent":["import { createInterface } from 'node:readline/promises'\n\nimport { createHostApp } from './create-host-app'\nimport { createDefaultAuthStore } from '@x-otto/ai'\nimport type { AuthStore, OAuthLoginOptions } from '@x-otto/ai'\n\n/**\n * `otto auth login|status|logout` 子命令(M8-07)。\n *\n * 驱动 AuthStore.login 的 onAuth/onPrompt/onManualCode 回调:\n * - Anthropic(usesCallbackServer=false)→ loopback(54545) 优先自动回填,\n * 端口占用时回退托管回调 + 用户回贴 code(onManualCode 必须提供);\n * - loopback provider(OpenAI/Codex,usesCallbackServer=true)→ 浏览器重定向自动完成。\n * - 插件 OAuth(cursor/anthropic 等)→ 须 bootstrap App 装载插件后再查 oauth 表(RFC-130 M131-1)。\n */\nexport interface AuthBootstrapOptions {\n workspaceDir: string\n projectConfigPath?: string\n verbose?: boolean\n}\n\nexport interface AuthCommandDeps {\n authStore?: AuthStore\n /** 未注入 authStore 时,经 App.start() 装载插件 OAuth(对齐 TUI `/auth`)。 */\n bootstrap?: AuthBootstrapOptions\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n /** 单行读取器(注入便于测试);未提供时按需创建 readline。 */\n prompt?: (question: string) => Promise<string>\n}\n\nexport async function runAuthCommand(args: string[], deps: AuthCommandDeps = {}): Promise<number> {\n if (!deps.authStore && deps.bootstrap) {\n const app = createHostApp({\n workspaceDir: deps.bootstrap.workspaceDir,\n projectConfigPath: deps.bootstrap.projectConfigPath,\n logger: {\n name: 'otto-auth',\n level: deps.bootstrap.verbose ? 'debug' : 'info',\n destination: 'stderr',\n },\n })\n try {\n await app.start()\n return await runAuthCommand(args, {\n ...deps,\n authStore: app.authStore,\n bootstrap: undefined,\n })\n } finally {\n await app.stop()\n }\n }\n\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n // watchForExternalChanges:false —— `otto auth *` 是一次性命令,跑完即退出,不需要跨进程凭据\n // 实时同步(那是给 TUI/serve 长驻进程用的)。AuthStore 的 fs.watch 句柄本身已 unref(不阻塞\n // 事件循环清空,见 auth-store.ts setupWatcher 注释),此处显式关闭仍是更准确的语义表达——\n // 避免为一个秒退的脚本无谓建立/拆除一个不会被用到的 watcher。\n const authStore = deps.authStore ?? createDefaultAuthStore({ watchForExternalChanges: false })\n\n const [action, provider] = args\n\n switch (action) {\n case 'login':\n return authLogin(authStore, provider, { stdout, stderr, prompt: deps.prompt })\n case 'logout':\n return authLogout(authStore, provider, { stdout, stderr })\n case 'status':\n case undefined:\n return authStatus(authStore, { stdout })\n default:\n stderr(`otto auth: unknown action '${action}'. Use login | status | logout.\\n`)\n return 1\n }\n}\n\nfunction loginableProviders(authStore: AuthStore): string[] {\n return authStore.oauth.list().map((p) => p.id)\n}\n\nasync function authLogin(\n authStore: AuthStore,\n provider: string | undefined,\n io: { stdout: (s: string) => void; stderr: (s: string) => void; prompt?: (q: string) => Promise<string> },\n): Promise<number> {\n const providers = loginableProviders(authStore)\n // UX(round 6):零 provider 插件时 providers 为空数组——\"One of: \"后面是空串,\n // 用户面对死端。指回引导路径而不是展示空名单。\n if (providers.length === 0) {\n io.stderr(\n 'otto auth login: no OAuth-capable providers available yet.\\n' +\n 'Install a provider plugin first — run `otto` (interactive) for guided setup,\\n' +\n 'or: otto extension install anthropic-provider\\n',\n )\n return 1\n }\n if (!provider) {\n io.stderr(`otto auth login: missing provider. One of: ${providers.join(', ')}\\n`)\n return 1\n }\n\n const oauth = authStore.oauth.get(provider)\n if (!oauth) {\n io.stderr(`otto auth login: unknown provider '${provider}'. One of: ${providers.join(', ')}\\n`)\n io.stderr(\n 'Hint: plugin providers (cursor, anthropic, …) need the plugin installed with provider capability granted.\\n' +\n ' otto extension install --link extensions/plugin-cursor --yes\\n' +\n ' # user-scope install auto-trusts; then otto auth login cursor\\n',\n )\n return 1\n }\n\n const rl = io.prompt ? undefined : createInterface({ input: process.stdin, output: process.stdout })\n const ask = io.prompt ?? ((q: string) => rl!.question(q))\n\n const controller = new AbortController()\n const onSigint = (): void => controller.abort()\n process.on('SIGINT', onSigint)\n\n try {\n const options: OAuthLoginOptions = {\n onAuth: (info) => {\n io.stdout(`\\nOpen this URL in your browser to authorize:\\n ${info.url}\\n`)\n if (info.code) {\n io.stdout(`Code: ${info.code}\\n`)\n }\n if (info.instructions) {\n io.stdout(`${info.instructions}\\n`)\n }\n },\n onPrompt: async (p) => {\n const suffix = p.placeholder ? ` (${p.placeholder})` : ''\n return (await ask(`${p.message}${suffix}: `)).trim()\n },\n // loopback 回调先完成时经 signal 放行。readline 本身无法中断,由 finally 的\n // rl.close() 收尾——close 会 reject 悬挂的 question,故 catch 归一为空串防 unhandled。\n onManualCode: async (signal?: AbortSignal) => {\n const question = ask('\\nPaste the code here: ').then(\n (s) => s.trim(),\n () => '',\n )\n if (!signal) return question\n if (signal.aborted) return ''\n return Promise.race([\n question,\n new Promise<string>((resolve) => {\n signal.addEventListener('abort', () => resolve(''), { once: true })\n }),\n ])\n },\n onProgress: (m) => io.stdout(`${m}\\n`),\n signal: controller.signal,\n }\n\n await authStore.login(provider, options)\n io.stdout(`\\n✓ Logged in to ${oauth.name}.\\n`)\n return 0\n } catch (error) {\n io.stderr(`\\notto auth login failed: ${(error as Error).message}\\n`)\n return 1\n } finally {\n process.off('SIGINT', onSigint)\n rl?.close()\n }\n}\n\nasync function authLogout(\n authStore: AuthStore,\n provider: string | undefined,\n io: { stdout: (s: string) => void; stderr: (s: string) => void },\n): Promise<number> {\n if (!provider) {\n io.stderr(\n `otto auth logout: missing provider. One of: ${authStore.listProviders().join(', ')}\\n`,\n )\n return 1\n }\n\n const had = await authStore.hasCredential(provider)\n await authStore.logout(provider)\n io.stdout(had ? `✓ Logged out of ${provider}.\\n` : `${provider} was not logged in.\\n`)\n return 0\n}\n\nconst SOURCE_LABEL: Record<string, string> = {\n oauth: 'OAuth',\n api_key: 'API key (stored)',\n env: 'API key (env)',\n}\n\nasync function authStatus(\n authStore: AuthStore,\n io: { stdout: (s: string) => void },\n): Promise<number> {\n // RFC-341 修复:`AuthStore.listProviders()` 是同步方法,读的是内存 cache——\n // cache 由 `ensureInitialized()`(异步读盘)填充,且没有任何调用方保证在\n // `listProviders()` 之前完成过初始化(`getAuthSource()` 等其余方法内部各自会\n // `await ensureInitialized()`,唯独 `listProviders()` 没有)。此前这里直接调\n // `listProviders()` 时 cache 大概率还是空的(首次调用、无其余方法先触发过初始化\n // 的一次性命令场景),导致 `otto auth status`/Setup Wizard 装完 provider 后\n // 立即查询状态永远显示\"未配置\"——即便凭据其实已经成功落盘(真实事故:OrbStack\n // 虚拟机全流程测试实测复现,auth.json 里确有凭据但 status/doctor 均报未配置)。\n await authStore.ensureInitialized()\n io.stdout('Authentication status:\\n')\n for (const provider of authStore.listProviders()) {\n const source = await authStore.getAuthSource(provider)\n const label = source ? SOURCE_LABEL[source] : 'not configured'\n io.stdout(` ${provider.padEnd(16)} ${label}\\n`)\n }\n return 0\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import"zod";function e(e){return e.startsWith(`backend/`)?e.slice(8):e}function t(t,n){return{call:async(r,i)=>{let a=e(r),o=t[a];if(!o)throw Error(`${n} backend: unknown method "${a}"`);let s=o.params.safeParse(i??{});if(!s.success)throw Error(`${n} backend: invalid params for "${a}": ${JSON.stringify(s.error.issues)}`);return o.run(s.data)}}}export{t};
3
+ //# sourceMappingURL=backend-handler-DUafuNWw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-handler-DUafuNWw.js","names":[],"sources":["../src/commands/backend-handler.ts"],"sourcesContent":["import { z } from 'zod'\n\n/**\n * backend-handler.ts —— RFC-439:in-process backend methods 类型化注册表。\n *\n * 收敛 feedback-backend-methods / schedule-backend-methods 的重复样板\n * (stripBackendPrefix 复制、switch 分发、`params as unknown as X` 双重断言):\n * 每个 backend 方法 = zod schema(参数形状 + 默认值单一真源)+ run 函数\n * (参数类型由 z.infer 推导)。面板每新增一个方法 = 表里加一条,不再复制样板。\n */\n\n/** 剥离 `backend/` 前缀(panel-backend-runtime 的 toBackendWireMethod 加的)。 */\nexport function stripBackendPrefix(method: string): string {\n return method.startsWith('backend/') ? method.slice('backend/'.length) : method\n}\n\n/**\n * 一个 backend 方法。`params` 是参数 zod schema(默认值经 .default() 内聚,\n * 非法参数在注册表边界 fail-closed);`run` 的执行参数类型由 schema 推导。\n */\nexport interface BackendMethod<TSchema extends z.ZodTypeAny = z.ZodTypeAny> {\n params: TSchema\n /**\n * run 参数显式 `any`:`Record<string, BackendMethod>` 会用默认泛型 `ZodTypeAny`,\n * `z.infer<ZodTypeAny>` 是 unknown——若 run 声明为 `(params: z.infer<TSchema>)`,\n * 字面量方法(run: (params: {...}) => ...)因逆变无法赋给 `(params: unknown)`。\n * `any` 在严格模式下允许具体签名赋给宽参数,运行期仍由 params.safeParse 把关。\n */\n run: (params: any) => Promise<unknown>\n}\n/**\n * 构造类型化 backend handler:统一前缀剥离 + 方法查表 + zod 参数校验 +\n * 未知方法 fail-closed(错误文案带宿主 label 便于排障)。\n */\nexport function createBackendHandler(\n methods: Record<string, BackendMethod>,\n label: string,\n): {\n call: (method: string, params: Record<string, unknown>) => Promise<unknown>\n} {\n const call = async (\n method: string,\n params: Record<string, unknown>,\n ): Promise<unknown> => {\n const name = stripBackendPrefix(method)\n const entry = methods[name]\n if (!entry) {\n throw new Error(`${label} backend: unknown method \"${name}\"`)\n }\n const parsed = entry.params.safeParse(params ?? {})\n if (!parsed.success) {\n throw new Error(\n `${label} backend: invalid params for \"${name}\": ${JSON.stringify(parsed.error.issues)}`,\n )\n }\n return entry.run(parsed.data)\n }\n return { call }\n}\n"],"mappings":";YAYA,SAAgB,EAAmB,EAAwB,CACzD,OAAO,EAAO,WAAW,WAAW,CAAG,EAAO,MAAM,EAAkB,CAAG,EAqB3E,SAAgB,EACd,EACA,EAGA,CAkBA,MAAO,CAAE,KAjBI,MACX,EACA,IACqB,CACrB,IAAM,EAAO,EAAmB,EAAO,CACjC,EAAQ,EAAQ,GACtB,GAAI,CAAC,EACH,MAAU,MAAM,GAAG,EAAM,4BAA4B,EAAK,GAAG,CAE/D,IAAM,EAAS,EAAM,OAAO,UAAU,GAAU,EAAE,CAAC,CACnD,GAAI,CAAC,EAAO,QACV,MAAU,MACR,GAAG,EAAM,gCAAgC,EAAK,KAAK,KAAK,UAAU,EAAO,MAAM,OAAO,GACvF,CAEH,OAAO,EAAM,IAAI,EAAO,KAAK,EAEhB"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ const e=5427,t=`9ae7b15d9`;export{e as n,t};
3
+ //# sourceMappingURL=build-id-DjE6eVpQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-id-DjE6eVpQ.js","names":[],"sources":["../src/build-id.ts"],"sourcesContent":["// auto-generated by scripts/gen-build-id.mjs — DO NOT EDIT\nexport const BUILD_ID = 5427\nexport const BUILD_BRANCH = 'dev'\nexport const BUILD_HASH = '9ae7b15d9'\n"],"mappings":";AACA,MAAa,EAAW,KAEX,EAAa"}
@@ -0,0 +1,24 @@
1
+ {
2
+ "id": "plugin-feedback",
3
+ "name": "Feedback",
4
+ "version": "0.1.0",
5
+ "description": "Feedback business contract (RFC-209): pulse survey recording (RFC-208) + GitHub issue submission (RFC-207) + dual-platform feedback panel (RFC-430). Renders no UI itself — TUI implements panels via `contributes.panels` and web-ui calls into this plugin's data/logic contract through FeedbackRegistry.",
6
+ "capabilities": ["feedback", "settings.read", "tui.renderer", "tui.command"],
7
+ "scripts": { "postinstall": "otto extension build" },
8
+ "contributes": {
9
+ "panels": [
10
+ {
11
+ "id": "feedback",
12
+ "title": "Feedback",
13
+ "entry": "panels-dist/feedback.mjs"
14
+ }
15
+ ],
16
+ "slashCommands": [
17
+ {
18
+ "name": "feedback",
19
+ "description": "Report an issue or send feedback via GitHub (/feedback [--repo <owner/repo>])",
20
+ "group": "other"
21
+ }
22
+ ]
23
+ }
24
+ }
@@ -0,0 +1,348 @@
1
+ /**
2
+ * feedback.tsx — RFC-430 M3:feedback 面板 Ink 组件。
3
+ *
4
+ * 面板走 plugin 的 `contributes.panels` 轴,由宿主 postinstall esbuild 编译为
5
+ * `panels-dist/feedback.mjs`。面板内数据/动作经 `usePanelBackend()` 桥接到宿主
6
+ * FeedbackRegistry(同 plugin-schedule 的 `usePanelBackend()` 模式)。
7
+ *
8
+ * 动作:n=新建 / m=标记已解决 / x=取消标记 / o=打开URL / r=刷新 / f=过滤 / q=关闭
9
+ */
10
+ import React, { useState, useEffect, useCallback, useMemo } from 'react'
11
+ import { Box, Text, useInput } from 'ink'
12
+ import { GroupDivider } from '@x-otto/tui'
13
+ import type { PanelTheme, PanelFigures } from './panel-host-globals'
14
+
15
+ interface FeedbackIssueRow {
16
+ source: 'github' | 'coding'
17
+ platformIssueId: string
18
+ title: string
19
+ state: 'open' | 'closed'
20
+ url: string
21
+ author?: string
22
+ createdAt?: string
23
+ updatedAt?: string
24
+ kind: 'issue' | 'discussion'
25
+ excerpt?: string
26
+ labels?: string[]
27
+ }
28
+
29
+ interface FeedbackListResult {
30
+ items: FeedbackIssueRow[]
31
+ unavailable?: Partial<Record<'github' | 'coding', string>>
32
+ }
33
+
34
+ interface FeedbackResolvedMark {
35
+ ts: number
36
+ source: 'github' | 'coding'
37
+ platformIssueId: string
38
+ url: string
39
+ title: string
40
+ }
41
+
42
+ type View = 'list' | 'new'
43
+ type FilterMode = 'all' | 'open' | 'resolved'
44
+
45
+ declare global {
46
+ function getTheme(): PanelTheme
47
+ function getFigures(): PanelFigures
48
+ function usePanelFocusClaimed(): boolean
49
+ function requestPanelClose(): void
50
+ function setPanelHint(hint: string | Array<[string, string]> | undefined): void
51
+ function t(key: string, opts?: Record<string, unknown>): string
52
+ interface PanelBackendHandle<T> {
53
+ state: T | undefined
54
+ status: 'connecting' | 'ready' | 'crashed' | 'disabled'
55
+ call: <P extends Record<string, unknown>, R>(method: string, params: P) => Promise<R>
56
+ }
57
+ function usePanelBackend<T>(): PanelBackendHandle<T>
58
+ }
59
+
60
+ const POLL_MS = 15000
61
+
62
+ const sourceBadge = (source: 'github' | 'coding', theme: PanelTheme): React.ReactElement => {
63
+ const label = source === 'github' ? 'GH' : 'COD'
64
+ const color = source === 'github' ? theme.success : theme.warning
65
+ return (
66
+ <Text key="badge" color={color} bold>
67
+ [{label}]
68
+ </Text>
69
+ )
70
+ }
71
+
72
+ function formatDate(iso?: string): string {
73
+ if (!iso) return ''
74
+ try {
75
+ return new Date(iso).toLocaleDateString()
76
+ } catch {
77
+ return iso
78
+ }
79
+ }
80
+
81
+ /** 行唯一 id(source + platformIssueId 组合,避免跨平台 id 碰撞)。 */
82
+ function rowId(row: FeedbackIssueRow): string {
83
+ return `${row.source}:${row.platformIssueId}`
84
+ }
85
+
86
+ export default function FeedbackPanel(): React.ReactElement {
87
+ const backend = usePanelBackend<unknown>()
88
+ const [view, setView] = useState<View>('list')
89
+ const [selectedIndex, setSelectedIndex] = useState(0)
90
+ const [filterMode, setFilterMode] = useState<FilterMode>('all')
91
+ const [listResult, setListResult] = useState<FeedbackListResult | undefined>(undefined)
92
+ const [resolvedMarks, setResolvedMarks] = useState<FeedbackResolvedMark[]>([])
93
+ const [platform, setPlatform] = useState<'github' | 'coding'>('github')
94
+ const [loading, setLoading] = useState(true)
95
+
96
+ // 新建表单状态(对齐 schedule 面板 create 表单范式:formFocus + 单字符输入收集)。
97
+ const [formTitle, setFormTitle] = useState('')
98
+ const [formRepro, setFormRepro] = useState('')
99
+ const [formExpected, setFormExpected] = useState('')
100
+ const [formActual, setFormActual] = useState('')
101
+ const [formFocus, setFormFocus] = useState(0) // 0=title, 1=repro, 2=expected, 3=actual
102
+ const [formMsg, setFormMsg] = useState<string | null>(null)
103
+ const [formMsgTone, setFormMsgTone] = useState<'success' | 'error' | null>(null)
104
+ const [submitting, setSubmitting] = useState(false)
105
+
106
+ const refresh = useCallback(async () => {
107
+ setLoading(true)
108
+ try {
109
+ const [listResult, resolvedResult, platformResult] = await Promise.all([
110
+ backend.call<Record<string, unknown>, FeedbackListResult>('feedback.listIssues', { state: 'all', limitPerSource: 20 }),
111
+ backend.call<Record<string, unknown>, { marks: FeedbackResolvedMark[] }>('feedback.listResolvedMarks', {}),
112
+ backend.call<Record<string, unknown>, { platform: 'github' | 'coding' }>('feedback.resolvePlatform', {}),
113
+ ])
114
+ setListResult(listResult)
115
+ setResolvedMarks(resolvedResult.marks ?? [])
116
+ setPlatform(platformResult.platform)
117
+ } catch {
118
+ // fail-soft:面板不因单次刷新失败而崩——保持上次数据 + loading=false。
119
+ } finally {
120
+ setLoading(false)
121
+ }
122
+ }, [backend])
123
+
124
+ useEffect(() => {
125
+ const timer = setInterval(refresh, POLL_MS)
126
+ refresh()
127
+ return () => clearInterval(timer)
128
+ }, [refresh])
129
+
130
+ const allRows = useMemo<FeedbackIssueRow[]>(() => listResult?.items ?? [], [listResult])
131
+
132
+ const resolvedSet = useMemo(() => {
133
+ const s = new Set<string>()
134
+ for (const m of resolvedMarks) s.add(`${m.source}:${m.platformIssueId}`)
135
+ return s
136
+ }, [resolvedMarks])
137
+
138
+ const filteredRows = useMemo(() => {
139
+ return allRows.filter((row) => {
140
+ const isResolved = resolvedSet.has(rowId(row))
141
+ if (filterMode === 'resolved') return isResolved
142
+ if (filterMode === 'open') return !isResolved
143
+ return true
144
+ })
145
+ }, [allRows, resolvedSet, filterMode])
146
+
147
+ useEffect(() => {
148
+ setSelectedIndex((i) => (filteredRows.length === 0 ? 0 : Math.min(i, filteredRows.length - 1)))
149
+ }, [filteredRows])
150
+
151
+ const markResolved = useCallback(async (row: FeedbackIssueRow) => {
152
+ await backend.call('feedback.markResolved', {
153
+ source: row.source,
154
+ platformIssueId: row.platformIssueId,
155
+ url: row.url,
156
+ title: row.title,
157
+ })
158
+ refresh()
159
+ }, [backend, refresh])
160
+
161
+ const unmarkResolved = useCallback(async (row: FeedbackIssueRow) => {
162
+ await backend.call('feedback.unmarkResolved', {
163
+ source: row.source,
164
+ platformIssueId: row.platformIssueId,
165
+ })
166
+ refresh()
167
+ }, [backend, refresh])
168
+
169
+ const submitForm = useCallback(async () => {
170
+ if (submitting) return
171
+ if (!formTitle.trim()) {
172
+ setFormMsg(t('feedbackPanel.formRequired'))
173
+ return
174
+ }
175
+ setSubmitting(true)
176
+ setFormMsg(t('feedbackPanel.formSubmitting'))
177
+ setFormMsgTone(null)
178
+ try {
179
+ const resp = await backend.call<
180
+ Record<string, unknown>,
181
+ { ok: boolean; url?: string; error?: string }
182
+ >('feedback.submitIssue', {
183
+ title: formTitle,
184
+ reproduce: formRepro || undefined,
185
+ expected: formExpected || undefined,
186
+ actual: formActual || undefined,
187
+ })
188
+ if (resp?.ok && resp.url) {
189
+ setFormMsg(t('feedbackPanel.formSuccess', { url: resp.url }))
190
+ setFormMsgTone('success')
191
+ setView('list')
192
+ } else if (resp?.error === 'repo-unresolved') {
193
+ setFormMsg(t('feedbackPanel.formRepoUnresolved'))
194
+ setFormMsgTone('error')
195
+ } else {
196
+ setFormMsg(t('feedbackPanel.formNoChannel'))
197
+ setFormMsgTone('error')
198
+ }
199
+ } catch {
200
+ setFormMsg(t('feedbackPanel.formNoChannel'))
201
+ setFormMsgTone('error')
202
+ } finally {
203
+ setSubmitting(false)
204
+ }
205
+ }, [backend, formTitle, formRepro, formExpected, formActual, submitting])
206
+
207
+ useInput((input, key) => {
208
+ if (view === 'new') {
209
+ if (key.escape || input === 'q') {
210
+ setView('list')
211
+ setFormMsg(null)
212
+ return
213
+ }
214
+ if (key.tab) {
215
+ setFormFocus((f) => (f + 1) % 4)
216
+ return
217
+ }
218
+ if (key.return) {
219
+ void submitForm()
220
+ return
221
+ }
222
+ if (key.backspace || key.delete) {
223
+ if (formFocus === 0) setFormTitle((v) => v.slice(0, -1))
224
+ else if (formFocus === 1) setFormRepro((v) => v.slice(0, -1))
225
+ else if (formFocus === 2) setFormExpected((v) => v.slice(0, -1))
226
+ else if (formFocus === 3) setFormActual((v) => v.slice(0, -1))
227
+ return
228
+ }
229
+ if (input && input.length === 1) {
230
+ if (formFocus === 0) setFormTitle((v) => v + input)
231
+ else if (formFocus === 1) setFormRepro((v) => v + input)
232
+ else if (formFocus === 2) setFormExpected((v) => v + input)
233
+ else if (formFocus === 3) setFormActual((v) => v + input)
234
+ }
235
+ return
236
+ }
237
+ if (key.downArrow || input === 'j') {
238
+ setSelectedIndex((i) => (filteredRows.length === 0 ? 0 : Math.min(i + 1, filteredRows.length - 1)))
239
+ }
240
+ if (key.upArrow || input === 'k') {
241
+ setSelectedIndex((i) => Math.max(i - 1, 0))
242
+ }
243
+ if (input === 'n') setView('new')
244
+ if (input === 'r') refresh()
245
+ if (input === 'f') setFilterMode((v) => (v === 'all' ? 'open' : v === 'open' ? 'resolved' : 'all'))
246
+ if (input === 'm' && filteredRows[selectedIndex]) {
247
+ const row = filteredRows[selectedIndex]
248
+ const isResolved = resolvedSet.has(rowId(row))
249
+ if (isResolved) unmarkResolved(row)
250
+ else markResolved(row)
251
+ }
252
+ if (input === 'x' && filteredRows[selectedIndex]) {
253
+ unmarkResolved(filteredRows[selectedIndex])
254
+ }
255
+ if (input === 'o' && filteredRows[selectedIndex]) {
256
+ // 打开 URL——面板进程内不可直接 spawn 浏览器,经宿主侧 backend 的
257
+ // feedback.openExternal(openBrowser)打开(RFC-430 D4)。
258
+ void backend.call('feedback.openExternal', { url: filteredRows[selectedIndex].url })
259
+ }
260
+ if (input === 'q' || key.escape) requestPanelClose()
261
+ })
262
+
263
+ const theme = getTheme()
264
+ const figures = getFigures()
265
+
266
+ useEffect(() => {
267
+ setPanelHint([
268
+ ['n', t('feedbackPanel.hintNew')],
269
+ ['m', t('feedbackPanel.hintMark')],
270
+ ['x', t('feedbackPanel.hintUnmark')],
271
+ ['o', t('feedbackPanel.hintOpen')],
272
+ ['r', t('feedbackPanel.hintRefresh')],
273
+ ['f', t('feedbackPanel.hintFilter')],
274
+ ['q', t('feedbackPanel.hintClose')],
275
+ ])
276
+ }, [])
277
+
278
+ const formRow = (label: string, value: string, focused: boolean, placeholder: string) => (
279
+ <Box flexDirection="row" gap={1}>
280
+ <Text dimColor={!focused}>{focused ? figures.pointer : ' '} {label}</Text>
281
+ <Text dimColor={!value && !focused}>{value || placeholder}</Text>
282
+ <Text color={theme.selection}>{value || (focused ? '_' : '')}</Text>
283
+ </Box>
284
+ )
285
+
286
+ if (view === 'new') {
287
+ return (
288
+ <Box flexDirection="column" gap={1}>
289
+ <Text bold>{t('feedbackPanel.newTitle')}</Text>
290
+ <Text dimColor>{t('feedbackPanel.newSubtitle', { platform })}</Text>
291
+ {formRow(t('feedbackPanel.formTitle'), formTitle, formFocus === 0, '')}
292
+ {formRow(t('feedbackPanel.formRepro'), formRepro, formFocus === 1, t('feedbackPanel.formRepro'))}
293
+ {formRow(t('feedbackPanel.formExpected'), formExpected, formFocus === 2, t('feedbackPanel.formExpected'))}
294
+ {formRow(t('feedbackPanel.formActual'), formActual, formFocus === 3, t('feedbackPanel.formActual'))}
295
+ {formMsg && (
296
+ <Text color={formMsgTone === 'success' ? theme.success : theme.warning}>
297
+ {formMsg}
298
+ </Text>
299
+ )}
300
+ <Text dimColor>{t('feedbackPanel.formHint')}</Text>
301
+ </Box>
302
+ )
303
+ }
304
+
305
+ return (
306
+ <Box flexDirection="column" gap={1}>
307
+ <Box justifyContent="space-between">
308
+ <Text bold>{t('feedbackPanel.title')}</Text>
309
+ <Text dimColor>{t('feedbackPanel.platform', { platform })}</Text>
310
+ </Box>
311
+ {loading && <Text dimColor>{t('feedbackPanel.loading')}</Text>}
312
+ {listResult?.unavailable &&
313
+ Object.entries(listResult.unavailable).map(([source, reason]) => (
314
+ <Box key={source}>
315
+ <Text color={theme.warning}>
316
+ {source}: {reason}
317
+ </Text>
318
+ </Box>
319
+ ))}
320
+ <Box flexDirection="column">
321
+ {filteredRows.length === 0 && !loading && (
322
+ <Text dimColor>{t('feedbackPanel.empty')}</Text>
323
+ )}
324
+ {filteredRows.map((row, i) => {
325
+ const isSelected = i === selectedIndex
326
+ const isResolved = resolvedSet.has(rowId(row))
327
+ return (
328
+ <Box key={rowId(row)} gap={1}>
329
+ {isSelected && <Text color={theme.selection}>{figures.pointer}</Text>}
330
+ {sourceBadge(row.source, theme)}
331
+ <Text dimColor={isResolved}>
332
+ {isResolved ? `${figures.check} ` : ''}{row.title}
333
+ </Text>
334
+ <Text dimColor>{formatDate(row.updatedAt ?? row.createdAt)}</Text>
335
+ <Text dimColor>{row.state}</Text>
336
+ </Box>
337
+ )
338
+ })}
339
+ </Box>
340
+ <GroupDivider label={t('feedbackPanel.legend')} />
341
+ <Box gap={2}>
342
+ <Text dimColor>{t('feedbackPanel.filter', { filter: filterMode })}</Text>
343
+ <Text dimColor>{t('feedbackPanel.total', { count: String(filteredRows.length) })}</Text>
344
+ </Box>
345
+ <Text dimColor>{t('feedbackPanel.resolvedNote')}</Text>
346
+ </Box>
347
+ )
348
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * panel-host-globals.d.ts — 插件面板宿主桥接全局类型声明(RFC-353 S12)。
3
+ *
4
+ * `panel-compiler.ts` 用 esbuild banner 把宿主运行时(`getTheme`/`getFigures`/
5
+ * `usePanelBackend`/`usePanelFocusClaimed`/`requestPanelClose`/`setPanelHint`/`t`)
6
+ * 注入为面板产物的全局引用(非常规 import,见 `packages/coding/src/plugin/panel-compiler.ts`
7
+ * 头注释)。此前每个面板 `.tsx` 文件各自手写一份 `declare function ...`(逐文件重复),
8
+ * 是漂移风险源——2026-08-11 review 实测发现 schedule.tsx 三处 `backend.call<TResult>()`
9
+ * 调用漏传 `TParams` 泛型参数(`call<TParams, TResult>` 签名下单参数会被错误绑定到
10
+ * `TParams`),typecheck 覆盖打开后才暴露(本文件与该修复同批落地)。
11
+ *
12
+ * 本文件用 `declare global` 环境声明(ambient declaration)替代逐文件重复:TS 编译器
13
+ * 按 tsconfig `include` 把本文件纳入同一程序,`declare global` 块内的声明对整个程序
14
+ * 全局可见,面板 `.tsx` 文件无需 import 即可直接使用这些函数名(与
15
+ * `packages/coding/src/global.d.ts` 声明 `globalThis.React` 同一模式)。
16
+ *
17
+ * 本文件是纯类型声明(zero JS 产出),且从不被任何 `.tsx` 文件 import——不会被
18
+ * esbuild 打包,只参与 tsc 类型检查程序。
19
+ */
20
+ export interface PanelTheme {
21
+ text: string | undefined
22
+ inactive: string
23
+ secondary: string
24
+ success: string
25
+ error: string
26
+ warning: string
27
+ special: string
28
+ selection: string
29
+ focusBorder: string
30
+ panelBorder: string
31
+ }
32
+
33
+ export interface PanelFigures {
34
+ check: string
35
+ cross: string
36
+ pointer: string
37
+ bullet: string
38
+ }
39
+
40
+ export type PanelBackendStatus = 'connecting' | 'ready' | 'crashed' | 'disabled'
41
+
42
+ export interface PanelBackendHandle<TState = unknown> {
43
+ /**
44
+ * `TParams` 必须显式指定(或从传入的 `params` 字面量推断)——本函数签名是
45
+ * `<TParams, TResult>`(不是 `<TResult>`)。调用方若只想指定返回类型,须写
46
+ * `call<Record<string, unknown>, TResult>(...)` 或让 TS 从 params 参数推断 TParams,
47
+ * 不能省略只传一个类型参数(会被错误绑定到 TParams 而非 TResult)。
48
+ */
49
+ call: <TParams extends Record<string, unknown>, TResult = unknown>(
50
+ method: string,
51
+ params: TParams,
52
+ opts?: { operationId?: string },
53
+ ) => Promise<TResult>
54
+ cancel: (operationId: string) => void
55
+ state: TState | undefined
56
+ status: PanelBackendStatus
57
+ }
58
+
59
+ declare global {
60
+ function getTheme(): PanelTheme
61
+ function getFigures(): PanelFigures
62
+ function usePanelFocusClaimed(): boolean
63
+ function requestPanelClose(): void
64
+ function setPanelHint(hint: string | Array<[string, string]> | undefined): void
65
+ function t(key: string, opts?: Record<string, unknown>): string
66
+ function usePanelBackend<TState = unknown>(): PanelBackendHandle<TState>
67
+ }