@visual-z/xeno 0.2.1 → 0.4.0

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 (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -9,7 +9,7 @@
9
9
 
10
10
  import type { PermissionRequest } from "../core/permissions.ts";
11
11
  import type { ApprovalChoice } from "../runtime/tools.ts";
12
- import type { SelectItem } from "../tui/select.ts";
12
+ import type { ListItem } from "../tui/ui/list-model.ts";
13
13
 
14
14
  export function approvalHeading(request: PermissionRequest): string {
15
15
  if (request.command !== undefined) {
@@ -24,8 +24,8 @@ export function approvalHeading(request: PermissionRequest): string {
24
24
  return `${request.tool} wants to run. Approval is the last look, not a boundary.`;
25
25
  }
26
26
 
27
- export function approvalItems(request: PermissionRequest): SelectItem[] {
28
- const items: SelectItem[] = [{ value: "once", label: "this once" }];
27
+ export function approvalItems(request: PermissionRequest): ListItem[] {
28
+ const items: ListItem[] = [{ value: "once", label: "this once" }];
29
29
  if (request.command !== undefined) {
30
30
  items.push({ value: "command", label: "this command this session" });
31
31
  }
package/src/apps/cli.ts CHANGED
@@ -19,8 +19,16 @@ import { createStartupInput } from "./startup-input.ts";
19
19
  import { runPrompt, runRepl } from "./shell.ts";
20
20
  import { runPackageCommand } from "../runtime/package-manager.ts";
21
21
  import { exportHtmlFile } from "./export-cli.ts";
22
+ import { CONTROL_SCHEMA } from "../core/control-protocol.ts";
23
+ import { sessionControl } from "../runtime/session-control.ts";
24
+ import { listenControl } from "./control-transport.ts";
25
+ import { runAppServer } from "./app-server.ts";
22
26
 
23
- const args = parseArgs(process.argv.slice(2), { deferExtensions: true });
27
+ const serverMode = process.argv[2] === "app-server";
28
+ if (serverMode && process.argv[3] === "schema") { console.log(JSON.stringify(CONTROL_SCHEMA, null, 2)); process.exit(0); }
29
+ const args = parseArgs(process.argv.slice(serverMode ? 3 : 2), { deferExtensions: true });
30
+ if (serverMode) { args.print = true; args.mode = "json"; if (args.prompt) throw Error("app-server receives prompts through turn/start"); }
31
+ let closeControl: (() => Promise<void>) | undefined;
24
32
  const input = createStartupInput({ interactive: isInteractiveSession(args) && !args.packageCommand && !args.version && !args.exportFile && !args.listModels && !args.modelsCommand });
25
33
  process.once("exit", input.restore);
26
34
  try {
@@ -43,22 +51,29 @@ try {
43
51
  finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
44
52
  } else {
45
53
  const app = await startup(args, input);
46
- if (args.print) {
47
- // With a prompt already given, stdin is optional context: wait briefly for it.
48
- args.prompt = mergePrintPrompt(args.prompt,
49
- await readPipedStdin(process.stdin, args.prompt.length > 0 ? { firstByteMs: 3_000 } : {}));
50
- }
51
- if (args.prompt) {
54
+ if (args.controlSocket) closeControl = await listenControl(args.controlSocket, () => sessionControl(app.session));
55
+ if (serverMode) {
52
56
  input.restore();
53
- await runPrompt(app, args.prompt);
57
+ await runAppServer(app);
54
58
  } else {
55
- input.phase("Loading conversation");
56
- await runRepl(app, input.handoff);
59
+ if (args.print) {
60
+ // With a prompt already given, stdin is optional context: wait briefly for it.
61
+ args.prompt = mergePrintPrompt(args.prompt,
62
+ await readPipedStdin(process.stdin, args.prompt.length > 0 ? { firstByteMs: 3_000 } : {}));
63
+ }
64
+ if (args.prompt) {
65
+ input.restore();
66
+ await runPrompt(app, args.prompt);
67
+ } else {
68
+ input.phase("Loading conversation");
69
+ await runRepl(app, input.handoff);
70
+ }
57
71
  }
58
72
  }
59
73
  } catch (error) {
60
74
  console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1;
61
75
  } finally {
76
+ await closeControl?.();
62
77
  process.off("exit", input.restore);
63
78
  input.restore();
64
79
  }
@@ -16,10 +16,17 @@ import { skillInvocationMessage, type Skill } from "../core/skills.ts";
16
16
 
17
17
  export type Command =
18
18
  | { kind: "extension"; name: string; args: string }
19
- | { kind: "prompt"; text: string }
19
+ | { kind: "prompt"; text: string; skill?: Skill }
20
20
  | { kind: "quit" }
21
21
  | { kind: "usage" }
22
22
  | { kind: "state" }
23
+ | { kind: "context" }
24
+ | { kind: "control" }
25
+ | { kind: "acceptance" }
26
+ | { kind: "tasks" }
27
+ | { kind: "branches"; action?: "wait" | "cancel" | "status"; node?: number }
28
+ | { kind: "processes" }
29
+ | { kind: "accept"; criterion: string }
23
30
  | { kind: "model"; pattern?: string }
24
31
  | { kind: "scoped-models" }
25
32
  | { kind: "login"; provider?: string }
@@ -54,7 +61,7 @@ export const COMMANDS = [
54
61
  { name: "scoped-models", description: "Enable and order models for cycling", busy: "idle" },
55
62
  { name: "thinking", description: "Set reasoning effort", argumentHint: "[level]", busy: "idle" },
56
63
  { name: "settings", description: "Configure appearance, input and model behavior", argumentHint: "[key value]", busy: "idle" },
57
- { name: "login", description: "Sign in or store an API key", argumentHint: "[provider]", busy: "idle" },
64
+ { name: "provider", description: "Connect and manage providers: account, API key, gateway or local server", argumentHint: "[provider]", busy: "idle" },
58
65
  { name: "logout", description: "Remove a stored credential", argumentHint: "[provider]", busy: "idle" },
59
66
  { name: "new", description: "Start a new session", busy: "idle" },
60
67
  { name: "resume", description: "Search saved sessions", argumentHint: "[id]", busy: "idle" },
@@ -66,13 +73,20 @@ export const COMMANDS = [
66
73
  { name: "session", description: "Show session details", busy: "run" },
67
74
  { name: "usage", description: "Show token usage and costs", busy: "run" },
68
75
  { name: "state", description: "Show log and handoff state", busy: "run" },
76
+ { name: "context", description: "Explain context sources, omissions and token estimates", busy: "run" },
77
+ { name: "control", description: "Inspect the goal, evidence and context; control the active turn", busy: "run" },
78
+ { name: "acceptance", description: "Show declared criteria and current evidence", busy: "idle" },
79
+ { name: "tasks", description: "Show work dependencies, readiness and ownership", busy: "run" },
80
+ { name: "branches", description: "Inspect or join owned background work", argumentHint: "[wait|cancel|status node]", busy: "idle" },
81
+ { name: "processes", description: "Show process observations and available handles", busy: "run" },
82
+ { name: "accept", description: "Record your acceptance of a human criterion", argumentHint: "<criterion ID>", busy: "idle" },
69
83
  { name: "export", description: "Export HTML or JSONL", argumentHint: "[file]", busy: "idle" },
70
84
  { name: "import", description: "Import a portable JSONL session", argumentHint: "<file>", busy: "idle" },
71
85
  { name: "debug", description: "Write local terminal diagnostics", argumentHint: "[file]", busy: "run" },
72
86
  { name: "changelog", description: "Show local release notes", busy: "run" },
73
87
  { name: "copy", description: "Copy the last assistant reply", busy: "run" },
74
88
  { name: "attachments", description: "Remove images from the current draft", busy: "run" },
75
- { name: "models", description: "Refresh the model catalog", argumentHint: "refresh", busy: "idle" },
89
+ { name: "models", description: "Refresh the model catalog", argumentHint: "<refresh>", busy: "idle" },
76
90
  { name: "trust", description: "Set workspace configuration trust", argumentHint: "[here|parent|never]", busy: "idle" },
77
91
  { name: "reload", description: "Reload settings, skills, prompts, themes and models", busy: "idle" },
78
92
  { name: "config", description: "Configure resource sources and enablement", busy: "idle" },
@@ -82,10 +96,18 @@ export const COMMANDS = [
82
96
  { name: "exit", description: "Exit xeno", busy: "run" },
83
97
  ] as const;
84
98
 
85
- export const BUILTIN_COMMAND_NAMES = COMMANDS.map((command) => command.name);
99
+ /**
100
+ * Hidden spellings of a listed command. Not in COMMANDS, so the `/` menu,
101
+ * the startup hint and completion show one entry per job.
102
+ */
103
+ const ALIASES: Readonly<Record<string, string>> = { login: "provider", connect: "provider" };
104
+
105
+ // Hidden spellings still own their names, so resources cannot promise an unreachable route.
106
+ export const BUILTIN_COMMAND_NAMES: readonly string[] = [...COMMANDS.map(command => command.name), ...Object.keys(ALIASES)];
86
107
 
87
108
  export function commandDefinition(input: string): (typeof COMMANDS)[number] | undefined {
88
- const name = input.trim().split(/\s/, 1)[0]?.slice(1).toLowerCase();
109
+ const typed = input.trim().split(/\s/, 1)[0]?.slice(1).toLowerCase() ?? "";
110
+ const name = ALIASES[typed] ?? typed;
89
111
  return COMMANDS.find((command) => command.name === name);
90
112
  }
91
113
 
@@ -95,6 +117,7 @@ export interface SlashCatalog {
95
117
  readonly prompts: ReadonlyMap<string, PromptTemplate>;
96
118
  readonly skills: ReadonlyMap<string, Skill>;
97
119
  readonly skillCommands?: boolean;
120
+ readonly skillExecution?: boolean;
98
121
  }
99
122
 
100
123
  export function resolveSlashCatalog(
@@ -140,7 +163,9 @@ export function parseCommand(input: string, catalog?: SlashCatalog): Command {
140
163
  if (!text.startsWith("/")) return { kind: "prompt", text };
141
164
 
142
165
  const [head, ...rest] = text.split(/\s+/);
143
- const token = (head ?? "").toLowerCase();
166
+ const typed = (head ?? "").toLowerCase();
167
+ const token = `/${ALIASES[typed.slice(1)] ?? typed.slice(1)}`;
168
+ if (token === "/control") return { kind: "control" };
144
169
  const arg = rest.join(" ");
145
170
 
146
171
  if (!commandDefinition(text)) return resolveRegistered(token, arg, text, rest.length, catalog);
@@ -157,9 +182,20 @@ export function parseCommand(input: string, catalog?: SlashCatalog): Command {
157
182
  return { kind: "usage" };
158
183
  case "/state":
159
184
  return { kind: "state" };
185
+ case "/context": return { kind: "context" };
186
+ case "/acceptance": return { kind: "acceptance" };
187
+ case "/tasks": return { kind: "tasks" };
188
+ case "/processes": return { kind: "processes" };
189
+ case "/branches": {
190
+ if (!arg) return { kind: "branches" };
191
+ const action = rest[0], node = Number(rest[1]);
192
+ return (action === "wait" || action === "cancel" || action === "status") && Number.isSafeInteger(node) && node >= 0 && rest.length === 2
193
+ ? { kind: "branches", action, node } : { kind: "unknown", input: text };
194
+ }
195
+ case "/accept": return { kind: "accept", criterion: arg };
160
196
  case "/model":
161
197
  return arg.length > 0 ? { kind: "model", pattern: arg } : { kind: "model" };
162
- case "/login":
198
+ case "/provider":
163
199
  return rest[0] === undefined ? { kind: "login" } : { kind: "login", provider: rest[0] };
164
200
  case "/logout":
165
201
  return rest[0] === undefined ? { kind: "logout" } : { kind: "logout", provider: rest[0] };
@@ -240,7 +276,8 @@ function resolveRegistered(
240
276
  const name = token.slice("/skill:".length);
241
277
  const skill = catalog.skills.get(name);
242
278
  if (skill !== undefined) {
243
- return { kind: "prompt", text: skillInvocationMessage(skill, arg) };
279
+ if (skill.userInvocable === false) return { kind: "unknown", input: text };
280
+ return { kind: "prompt", text: skillInvocationMessage(skill, arg), ...(catalog.skillExecution ? { skill } : {}) };
244
281
  }
245
282
  }
246
283
  const prompt = catalog.prompts.get(token.slice(1));
@@ -1,31 +1,87 @@
1
- import { COMMANDS } from "./commands.ts";
1
+ /**
2
+ * What the composer offers for the text at the cursor. Pure given its
3
+ * sources, so the three cases are tested without a terminal:
4
+ * "/mo|" command names (the palette's own entries)
5
+ * "/thinking h|" the command's argument values
6
+ * "see @src/ap|" workspace paths (ranked fuzzy, quoted when they contain spaces)
7
+ * `@` inside a word (an e-mail) or after a backslash is not a reference.
8
+ */
2
9
  import type { App } from "./startup.ts";
3
- import { WorkspaceAutocomplete } from "../tui/autocomplete.ts";
4
- import { SUBSCRIPTION_OPTIONS } from "../runtime/oauth.ts";
5
- import { modelKey } from "../core/session-position.ts";
10
+ import { SUBSCRIPTION_OPTIONS, subscriptionById } from "../runtime/oauth.ts";
6
11
  import { supportedThinkingLevels } from "../core/thinking.ts";
7
- import { SETTINGS_KEYS, DISPLAY_SETTINGS } from "../core/settings.ts";
12
+ import { interactiveSettingKeys } from "../core/settings.ts";
13
+ import { fuzzyFilter } from "../tui/fuzzy.ts";
14
+ import { COMMANDS, commandDefinition } from "./commands.ts";
15
+ import { commandCompletionRank } from "../core/completion-action.ts";
16
+ import type { Completion } from "../tui/ui/composer.ts";
17
+ import type { ListItem } from "../tui/ui/list-model.ts";
8
18
 
9
- export function interactiveAutocomplete(app: App, paths: (refresh?: boolean) => Promise<readonly string[]>): WorkspaceAutocomplete {
10
- const providers = [...new Set([...app.catalog.map((m) => m.provider), ...SUBSCRIPTION_OPTIONS.map((p) => p.id)])];
19
+ export interface CompletionSources {
20
+ readonly commands: readonly { name: string; description: string }[];
21
+ readonly argumentValues: (command: string) => readonly string[];
22
+ readonly paths: readonly string[];
23
+ }
24
+
25
+ /** Twelve rows: enough to see siblings of a path without hiding the composer. Assumed. */
26
+ const MAX_ITEMS = 12;
27
+
28
+ export function completionAt(text: string, cursor: number, sources: CompletionSources): Completion | undefined {
29
+ const before = text.slice(0, cursor);
30
+ const line = before.slice(before.lastIndexOf("\n") + 1);
31
+ const lineStart = cursor - line.length;
32
+ if (line.startsWith("/") && lineStart === 0) {
33
+ const space = line.indexOf(" ");
34
+ if (space < 0) {
35
+ const query = line.slice(1).toLowerCase();
36
+ const items = sources.commands
37
+ .map(command => ({ command, rank: commandCompletionRank(command.name, query) }))
38
+ .filter(item => Number.isFinite(item.rank)).sort((a, b) => a.rank - b.rank)
39
+ .map(({ command }) => ({ value: `/${command.name} `, label: `/${command.name}`, description: command.description }));
40
+ return items.length ? { kind: "command", start: 0, end: cursor, items: items.slice(0, MAX_ITEMS) } : undefined;
41
+ }
42
+ const command = line.slice(1, space).toLowerCase();
43
+ const arg = line.slice(space + 1);
44
+ if (arg.includes(" ")) return undefined;
45
+ const values = sources.argumentValues(command);
46
+ if (!values.length) return undefined;
47
+ const items = fuzzyFilter(values, arg).slice(0, MAX_ITEMS).map(value => ({ value, label: value }));
48
+ return items.length ? { kind: "argument", start: space + 1, end: cursor, items } : undefined;
49
+ }
50
+ const at = line.lastIndexOf("@");
51
+ if (at < 0) return undefined;
52
+ const prev = line[at - 1];
53
+ if (prev !== undefined && !/\s/.test(prev)) return undefined;
54
+ const query = line.slice(at + 1);
55
+ if (/\s/.test(query) && !query.startsWith("\"")) return undefined;
56
+ const bare = query.replace(/^"/, "");
57
+ const items: ListItem[] = fuzzyFilter(sources.paths, bare).slice(0, MAX_ITEMS)
58
+ .map(path => ({ value: `@${/\s/.test(path) ? JSON.stringify(path) : path} `, label: path }));
59
+ return items.length ? { kind: "path", start: lineStart + at, end: cursor, items } : undefined;
60
+ }
61
+
62
+ export function completionSources(app: App, paths: readonly string[]): CompletionSources {
63
+ const providers = [...new Set([...app.catalog.map(m => m.provider).filter(provider => !subscriptionById(provider)), ...SUBSCRIPTION_OPTIONS.map(p => p.id)])];
11
64
  const values: Record<string, () => string[]> = {
12
- model: () => app.catalog.map(modelKey),
65
+ // Model identities belong to the searchable picker. The generic completer
66
+ // still handles arbitrary argument sources; this app supplies only one
67
+ // model selection surface, rather than a second list with different keys.
13
68
  thinking: () => [...supportedThinkingLevels(app.providerConfig.spec)],
14
- login: () => providers, logout: () => providers,
15
- settings: () => [...new Set([...SETTINGS_KEYS, ...DISPLAY_SETTINGS.map(item => item.id)])],
69
+ provider: () => providers, logout: () => Object.keys(app.auth).filter(provider => app.auth[provider] !== undefined),
70
+ settings: interactiveSettingKeys,
16
71
  models: () => ["refresh"], trust: () => ["once", "here", "parent", "never"],
17
72
  };
18
- const commands = COMMANDS.map((definition) => ({
19
- ...definition,
20
- getArgumentCompletions(prefix: string) {
21
- return (values[definition.name]?.() ?? []).filter((v) => v.toLowerCase().includes(prefix.toLowerCase()))
22
- .map((v) => ({ value: v, label: v }));
23
- },
24
- }));
25
- const registered = [
26
- ...[...app.extensions?.commands ?? []].map(([name, command]) => ({ name, ...command })),
27
- ...[...(app.slash?.prompts.values() ?? [])].map((p) => ({ name: p.name, description: p.description })),
28
- ...(app.slash?.skillCommands === false ? [] : [...(app.slash?.skills.values() ?? [])].map((s) => ({ name: `skill:${s.name}`, description: s.description }))),
73
+ return {
74
+ commands: [...COMMANDS.filter(command => command.name !== "exit"), ...commandEntries(app)],
75
+ argumentValues: command => values[commandDefinition(`/${command}`)?.name ?? command.toLowerCase()]?.() ?? [],
76
+ paths,
77
+ };
78
+ }
79
+
80
+ /** Built-ins, then extension commands, prompt templates and skills: the same names the palette lists. */
81
+ export function commandEntries(app: App): { name: string; description: string; section: string }[] {
82
+ return [
83
+ ...[...app.extensions?.commands ?? []].map(([name, command]) => ({ name, description: command.description ?? "extension command", section: "Extensions" })),
84
+ ...[...(app.slash?.prompts.values() ?? [])].map(p => ({ name: p.name, description: p.description ?? "prompt template", section: "Prompts" })),
85
+ ...(app.slash?.skillCommands === false ? [] : [...(app.slash?.skills.values() ?? [])].map(s => ({ name: `skill:${s.name}`, description: s.description ?? "skill", section: "Skills" }))),
29
86
  ];
30
- return new WorkspaceAutocomplete([...commands, ...registered], app.workspace, paths);
31
87
  }
@@ -0,0 +1,89 @@
1
+ /** Q24: bounded local JSONL transport. No TCP listener or additional runtime dependency. */
2
+ import { createServer, type Socket } from "node:net";
3
+ import { chmodSync } from "node:fs";
4
+ import { lstat, mkdir, rm } from "node:fs/promises";
5
+ import { dirname, resolve } from "node:path";
6
+ import { CONTROL_SCHEMA } from "../core/control-protocol.ts";
7
+ import { ControlConnection, type RpcOutput } from "../runtime/control-connection.ts";
8
+ import type { SessionControl } from "../runtime/session-control.ts";
9
+
10
+ export class ControlJsonl {
11
+ private buffer = Buffer.alloc(0);
12
+ private pending = 0;
13
+ private tail = Promise.resolve();
14
+ private closed = false;
15
+ readonly connection: ControlConnection;
16
+ private readonly stop: () => void;
17
+ private readonly output: (text: string) => void;
18
+ constructor(control: () => SessionControl, output: (text: string) => void, stop: () => void, denyOnClose = false) {
19
+ this.stop = stop;
20
+ this.output = text => { try { output(text); } catch { this.close(); this.stop(); } };
21
+ this.connection = new ControlConnection(control, message => this.write(message), { denyOnClose });
22
+ }
23
+ private write(message: RpcOutput): void {
24
+ if (this.closed) return;
25
+ const text = JSON.stringify(message) + "\n";
26
+ if (Buffer.byteLength(text) > CONTROL_SCHEMA.limits.frameBytes) {
27
+ if ("id" in message) this.output(JSON.stringify({ id: message.id, error: { code: -32002, message: "response exceeds frame limit; use paginated inspectors" } }) + "\n");
28
+ else this.fail("event exceeds frame limit; reconnect and resynchronize");
29
+ } else this.output(text);
30
+ }
31
+ private fail(message: string): void {
32
+ if (this.closed) return;
33
+ this.output(JSON.stringify({ id: null, error: { code: -32001, message } }) + "\n");
34
+ this.close(); this.stop();
35
+ }
36
+ feed(chunk: Uint8Array): void {
37
+ if (this.closed) return;
38
+ this.buffer = Buffer.concat([this.buffer, chunk]);
39
+ for (;;) {
40
+ const newline = this.buffer.indexOf(10);
41
+ if (newline < 0) { if (this.buffer.length > CONTROL_SCHEMA.limits.frameBytes) this.fail("input frame exceeds limit"); return; }
42
+ if (newline > CONTROL_SCHEMA.limits.frameBytes) { this.fail("input frame exceeds limit"); return; }
43
+ const line = this.buffer.subarray(0, newline).toString("utf8"); this.buffer = this.buffer.subarray(newline + 1);
44
+ if (!line.trim()) continue;
45
+ if (++this.pending > CONTROL_SCHEMA.limits.queuedRequests) { this.fail("request queue is full"); return; }
46
+ this.tail = this.tail.then(async () => {
47
+ if (this.closed) return;
48
+ let value: unknown;
49
+ try { value = JSON.parse(line); }
50
+ catch { this.write({ id: null, error: { code: -32700, message: "invalid JSON" } }); return; }
51
+ await this.connection.receive(value);
52
+ }).finally(() => { this.pending--; });
53
+ }
54
+ }
55
+ async drained(): Promise<void> { await this.tail; }
56
+ close(): void { this.closed = true; this.buffer = Buffer.alloc(0); this.connection.close(); }
57
+ }
58
+
59
+ export async function listenControl(path: string, control: () => SessionControl): Promise<() => Promise<void>> {
60
+ const absolute = resolve(path), parent = dirname(absolute);
61
+ await mkdir(parent, { recursive: true, mode: 0o700 });
62
+ const directory = await lstat(parent);
63
+ if (!directory.isDirectory() || directory.mode & 0o077 || process.getuid && directory.uid !== process.getuid()) throw Error("control socket requires an owned private directory (mode 0700)");
64
+ try { await lstat(absolute); throw Error("control socket path already exists; refusing to replace it"); }
65
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
66
+ const clients = new Map<Socket, ControlJsonl>();
67
+ const server = createServer(socket => {
68
+ // Assumed local adapter capacity; together with per-peer bounds this caps retained transport memory.
69
+ if (clients.size >= 32) { socket.destroy(); return; }
70
+ const peer = new ControlJsonl(control, text => {
71
+ if (socket.writableLength + Buffer.byteLength(text) > CONTROL_SCHEMA.limits.outputBytes) socket.destroy(); else socket.write(text);
72
+ }, () => socket.destroy());
73
+ clients.set(socket, peer);
74
+ socket.on("data", chunk => peer.feed(typeof chunk === "string" ? Buffer.from(chunk) : chunk));
75
+ socket.on("error", () => socket.destroy());
76
+ socket.on("close", () => { peer.close(); clients.delete(socket); });
77
+ });
78
+ await new Promise<void>((resolveReady, reject) => {
79
+ server.once("error", reject);
80
+ server.listen(absolute, () => { chmodSync(absolute, 0o600); resolveReady(); });
81
+ });
82
+ const owned = await lstat(absolute);
83
+ return async () => {
84
+ for (const [socket, peer] of clients) { peer.close(); socket.destroy(); }
85
+ await new Promise<void>((resolveClosed, reject) => server.close(error => error ? reject(error) : resolveClosed()));
86
+ try { const current = await lstat(absolute); if (current.ino === owned.ino && current.dev === owned.dev) await rm(absolute); }
87
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
88
+ };
89
+ }
@@ -0,0 +1,46 @@
1
+ import { draftImageInput, draftImageSpans, focusedDraftImage, imageMarker } from "../core/draft-images.ts";
2
+ import type { ImageRef } from "../core/images.ts";
3
+
4
+ /** D19: the renderer owns text and undo; the app only binds tokens to D8 blob identities. */
5
+ export interface ImageDraftEditor {
6
+ readonly text: string;
7
+ readonly cursor: number;
8
+ setAtoms(spans: (text: string) => readonly { start: number; end: number }[]): void;
9
+ insert(text: string): void;
10
+ removeAtom(label: string): void;
11
+ resetDraft(project: (text: string) => string): void;
12
+ }
13
+
14
+ export class ImageDraft {
15
+ private readonly editor: ImageDraftEditor;
16
+ private readonly registry = new Map<number, ImageRef>();
17
+ // Never reuse an ID: undo, kill/yank and local history can restore an earlier tag.
18
+ private nextId = 1;
19
+
20
+ constructor(editor: ImageDraftEditor) {
21
+ this.editor = editor;
22
+ editor.setAtoms(text => draftImageSpans(text, this.registry));
23
+ }
24
+ get images(): ImageRef[] { return this.input(this.editor.text).images; }
25
+ get focusedImage() { return focusedDraftImage(this.editor.text, this.editor.cursor, this.registry); }
26
+ input(text: string) { return draftImageInput(text, this.registry); }
27
+
28
+ insertImages(images: readonly ImageRef[]): void {
29
+ let labels = "";
30
+ for (const image of images) {
31
+ while (this.editor.text.includes(imageMarker(this.nextId))) this.nextId++;
32
+ const id = this.nextId++;
33
+ this.registry.set(id, image); labels += imageMarker(id);
34
+ }
35
+ if (labels) this.editor.insert(labels);
36
+ }
37
+ removeImage(index: number): void {
38
+ const ids = [...new Set(draftImageSpans(this.editor.text, this.registry).map(span => span.id))];
39
+ const id = ids[index];
40
+ if (id !== undefined) this.editor.removeAtom(imageMarker(id));
41
+ }
42
+ reset(): void {
43
+ this.editor.resetDraft(text => this.input(text).text);
44
+ this.registry.clear();
45
+ }
46
+ }
@@ -1,6 +1,6 @@
1
1
  import { modelKey } from "../core/session-position.ts";
2
2
  import { clampThinkingLevel } from "../core/thinking.ts";
3
- import { createProvider } from "../runtime/catalog.ts";
3
+ import { createProvider, providerSessionId } from "../runtime/catalog.ts";
4
4
  import { storedIsOAuth } from "../runtime/auth.ts";
5
5
  import { outputTokenBudget, type ModelSpec } from "../runtime/model.ts";
6
6
  import { providerAccess, setProviderAccess } from "../runtime/provider-access.ts";
@@ -29,6 +29,7 @@ export async function publishModelCatalog(app: App, models: readonly ModelSpec[]
29
29
  const config = { ...app.providerConfig, spec, maxTokens: outputTokenBudget(spec), effort: clampThinkingLevel(spec, app.providerConfig.effort) };
30
30
  const credential = app.credentialOf(spec);
31
31
  const next = createProvider(config, credential.ok ? credential.key : "", retryHandler(app.io), {
32
+ sessionId: providerSessionId(app.session.config.logDir),
32
33
  effects: app.effects, oauth: !app.args.apiKey && storedIsOAuth(app.auth, spec.provider),
33
34
  });
34
35
  const needsCut = providerReplayKey(previous) !== providerReplayKey(spec);
@@ -1,49 +1,9 @@
1
+ /** Project trust decisions; the picker itself is apps/ui/standalone.ts (startup) and apps/ui/app.ts (/trust). */
1
2
  import { dirname } from "node:path";
2
- import { SelectList } from "../tui/select.ts";
3
- import { createInteractiveTui } from "../tui/host.ts";
4
- import { IDENTITY } from "../tui/style.ts";
5
- import { installBindings } from "../tui/input.ts";
6
- import { loadUiResources } from "../store/resources.ts";
7
- import type { Style } from "../tui/style.ts";
8
- import { projectTrust, saveTrust } from "../store/trust.ts";
9
- import { truncateToWidth } from "../vendor/pi-tui/utils.ts";
3
+ import { saveTrust } from "../store/trust.ts";
10
4
 
11
5
  export type TrustChoice = "once" | "here" | "parent" | "never";
12
6
  export async function applyTrustChoice(workspace: string, choice: TrustChoice): Promise<boolean> {
13
7
  if (choice !== "once") await saveTrust(choice === "parent" ? dirname(workspace) : workspace, choice !== "never");
14
8
  return choice !== "never";
15
9
  }
16
- export class TrustSelector extends SelectList {
17
- private readonly workspace: string;
18
- private readonly decision: Awaited<ReturnType<typeof projectTrust>>;
19
- constructor(workspace: string, decision: Awaited<ReturnType<typeof projectTrust>>, style: Style) {
20
- super([
21
- { value: "once", label: "Trust for this session", description: "Load project resources without saving a decision" },
22
- { value: "here", label: "Trust this directory", description: workspace },
23
- { value: "parent", label: "Trust parent directory", description: dirname(workspace) },
24
- { value: "never", label: "Do not trust this directory", description: "Skip project configuration and executable extensions" },
25
- ], { title: "Project trust", search: false, style,
26
- ...(decision.saved ? { selectedValue: decision.saved.trusted ? decision.saved.path === workspace ? "here" : "parent" : "never" } : {}) });
27
- this.workspace = workspace; this.decision = decision;
28
- }
29
- override render(width: number): string[] {
30
- const saved = this.decision.saved;
31
- return ["Project trust", this.workspace, `Saved: ${saved ? `${saved.trusted ? "trusted" : "untrusted"} (${saved.path})` : "none"}`,
32
- `Current session: ${this.decision.trusted ? "trusted" : "untrusted"} · Tool permissions are separate`, ...super.render(width).slice(1)]
33
- .map(line => truncateToWidth(line, width, ""));
34
- }
35
- }
36
- export async function pickProjectTrust(workspace: string, handoff?: () => void): Promise<boolean> {
37
- const decision = await projectTrust(workspace);
38
- installBindings((await loadUiResources({ workspace, trusted: false })).keymap.byAction);
39
- const screen = createInteractiveTui();
40
- const select = new TrustSelector(workspace, decision, IDENTITY);
41
- screen.addChild(select); screen.setFocus(select);
42
- try {
43
- const choice = await new Promise<string | null>(resolve => {
44
- select.onInput = data => { const state = select.acceptInput(data); if (state.cancelled) resolve(null); else if (state.confirmed) resolve(state.confirmed.value); screen.requestRender(); };
45
- handoff?.(); screen.start();
46
- });
47
- return choice === null ? false : applyTrustChoice(workspace, choice as TrustChoice);
48
- } finally { await screen.terminal.drainInput(); screen.stop(); }
49
- }