@bastani/atomic 0.8.28-alpha.2 → 0.8.28-alpha.4

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 (117) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/CHANGELOG.md +24 -0
  7. package/dist/builtin/workflows/README.md +1 -1
  8. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +2 -1
  9. package/dist/builtin/workflows/builtin/goal.ts +2 -1
  10. package/dist/builtin/workflows/builtin/open-claude-design.ts +2 -1
  11. package/dist/builtin/workflows/builtin/ralph.ts +4 -2
  12. package/dist/builtin/workflows/package.json +1 -1
  13. package/dist/builtin/workflows/src/authoring.d.ts +5 -2
  14. package/dist/builtin/workflows/src/extension/dispatcher.ts +2 -0
  15. package/dist/builtin/workflows/src/extension/index.ts +8 -0
  16. package/dist/builtin/workflows/src/extension/render-result.ts +5 -2
  17. package/dist/builtin/workflows/src/extension/workflow-schema.ts +18 -0
  18. package/dist/builtin/workflows/src/runs/background/status.ts +4 -0
  19. package/dist/builtin/workflows/src/runs/foreground/executor.ts +1251 -110
  20. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +34 -10
  21. package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +10 -2
  22. package/dist/builtin/workflows/src/shared/persistence-restore.ts +28 -9
  23. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +9 -3
  24. package/dist/builtin/workflows/src/shared/store-types.ts +10 -3
  25. package/dist/builtin/workflows/src/shared/store.ts +29 -7
  26. package/dist/builtin/workflows/src/shared/types.ts +12 -10
  27. package/dist/builtin/workflows/src/tui/run-detail.ts +12 -0
  28. package/dist/builtin/workflows/src/tui/status-helpers.ts +4 -0
  29. package/dist/builtin/workflows/src/tui/status-list.ts +15 -1
  30. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +1 -1
  31. package/dist/builtin/workflows/src/tui/widget.ts +12 -3
  32. package/dist/builtin/workflows/src/workflows/define-workflow.ts +3 -3
  33. package/dist/core/agent-session-services.d.ts +1 -0
  34. package/dist/core/agent-session-services.d.ts.map +1 -1
  35. package/dist/core/agent-session-services.js +1 -0
  36. package/dist/core/agent-session-services.js.map +1 -1
  37. package/dist/core/agent-session.d.ts +4 -0
  38. package/dist/core/agent-session.d.ts.map +1 -1
  39. package/dist/core/agent-session.js +12 -1
  40. package/dist/core/agent-session.js.map +1 -1
  41. package/dist/core/index.d.ts +1 -0
  42. package/dist/core/index.d.ts.map +1 -1
  43. package/dist/core/index.js +1 -0
  44. package/dist/core/index.js.map +1 -1
  45. package/dist/core/sdk.d.ts +4 -2
  46. package/dist/core/sdk.d.ts.map +1 -1
  47. package/dist/core/sdk.js +1 -0
  48. package/dist/core/sdk.js.map +1 -1
  49. package/dist/core/tools/ask-user-question/state/inline-input.d.ts +28 -0
  50. package/dist/core/tools/ask-user-question/state/inline-input.d.ts.map +1 -0
  51. package/dist/core/tools/ask-user-question/state/inline-input.js +56 -0
  52. package/dist/core/tools/ask-user-question/state/inline-input.js.map +1 -0
  53. package/dist/core/tools/ask-user-question/state/key-router.d.ts.map +1 -1
  54. package/dist/core/tools/ask-user-question/state/key-router.js +30 -4
  55. package/dist/core/tools/ask-user-question/state/key-router.js.map +1 -1
  56. package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts.map +1 -1
  57. package/dist/core/tools/ask-user-question/state/questionnaire-session.js +9 -8
  58. package/dist/core/tools/ask-user-question/state/questionnaire-session.js.map +1 -1
  59. package/dist/core/tools/ask-user-question/state/row-intent.d.ts +3 -2
  60. package/dist/core/tools/ask-user-question/state/row-intent.d.ts.map +1 -1
  61. package/dist/core/tools/ask-user-question/state/row-intent.js +1 -1
  62. package/dist/core/tools/ask-user-question/state/row-intent.js.map +1 -1
  63. package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts +2 -0
  64. package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts.map +1 -1
  65. package/dist/core/tools/ask-user-question/state/selectors/contract.js.map +1 -1
  66. package/dist/core/tools/ask-user-question/state/selectors/projections.d.ts.map +1 -1
  67. package/dist/core/tools/ask-user-question/state/selectors/projections.js +2 -0
  68. package/dist/core/tools/ask-user-question/state/selectors/projections.js.map +1 -1
  69. package/dist/core/tools/ask-user-question/state/state-reducer.d.ts.map +1 -1
  70. package/dist/core/tools/ask-user-question/state/state-reducer.js +36 -24
  71. package/dist/core/tools/ask-user-question/state/state-reducer.js.map +1 -1
  72. package/dist/core/tools/ask-user-question/state/state.d.ts +8 -0
  73. package/dist/core/tools/ask-user-question/state/state.d.ts.map +1 -1
  74. package/dist/core/tools/ask-user-question/state/state.js.map +1 -1
  75. package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +6 -0
  76. package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
  77. package/dist/core/tools/ask-user-question/tool/format-answer.js +19 -1
  78. package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
  79. package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +3 -2
  80. package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
  81. package/dist/core/tools/ask-user-question/tool/response-envelope.js +15 -3
  82. package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
  83. package/dist/core/tools/ask-user-question/tool/types.d.ts +2 -1
  84. package/dist/core/tools/ask-user-question/tool/types.d.ts.map +1 -1
  85. package/dist/core/tools/ask-user-question/tool/types.js.map +1 -1
  86. package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts +5 -2
  87. package/dist/core/tools/ask-user-question/view/components/chat-row-view.d.ts.map +1 -1
  88. package/dist/core/tools/ask-user-question/view/components/chat-row-view.js +2 -0
  89. package/dist/core/tools/ask-user-question/view/components/chat-row-view.js.map +1 -1
  90. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts +1 -0
  91. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
  92. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +2 -1
  93. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
  94. package/dist/core/tools/ask-user-question/view/props-adapter.d.ts +3 -3
  95. package/dist/core/tools/ask-user-question/view/props-adapter.d.ts.map +1 -1
  96. package/dist/core/tools/ask-user-question/view/props-adapter.js +11 -4
  97. package/dist/core/tools/ask-user-question/view/props-adapter.js.map +1 -1
  98. package/dist/core/tools/bash-policy.d.ts +62 -0
  99. package/dist/core/tools/bash-policy.d.ts.map +1 -0
  100. package/dist/core/tools/bash-policy.js +1069 -0
  101. package/dist/core/tools/bash-policy.js.map +1 -0
  102. package/dist/core/tools/bash.d.ts +5 -0
  103. package/dist/core/tools/bash.d.ts.map +1 -1
  104. package/dist/core/tools/bash.js +7 -0
  105. package/dist/core/tools/bash.js.map +1 -1
  106. package/dist/core/tools/index.d.ts +1 -0
  107. package/dist/core/tools/index.d.ts.map +1 -1
  108. package/dist/core/tools/index.js +1 -0
  109. package/dist/core/tools/index.js.map +1 -1
  110. package/dist/index.d.ts +2 -2
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +1 -1
  113. package/dist/index.js.map +1 -1
  114. package/docs/sdk.md +42 -0
  115. package/docs/security.md +2 -0
  116. package/docs/workflows.md +127 -15
  117. package/package.json +1 -1
@@ -4,8 +4,9 @@ import { type WrappingSelectItem, type WrappingSelectTheme } from "./wrapping-se
4
4
  /**
5
5
  * Per-tick projection of chat-row state. The chat row is a single-item
6
6
  * `WrappingSelect` rendered in the question-tab footer; it owns no per-tab
7
- * state — only `focused` (whether the chat row is the active focus target)
8
- * and `numbering` (display number aligned with the active tab's items).
7
+ * state — only `focused` (whether the chat row is the active focus target),
8
+ * `numbering` (display number aligned with the active tab's items), and the
9
+ * owner-specific inline input projection supplied by the session.
9
10
  */
10
11
  export interface ChatRowViewProps {
11
12
  focused: boolean;
@@ -13,6 +14,8 @@ export interface ChatRowViewProps {
13
14
  offset: number;
14
15
  total: number;
15
16
  };
17
+ inputBuffer: string;
18
+ inputCaret: number;
16
19
  }
17
20
  export interface ChatRowViewConfig {
18
21
  /** The single chat sentinel row — `{kind: "chat", label: SENTINEL_LABELS.chat}`. */
@@ -1 +1 @@
1
- {"version":3,"file":"chat-row-view.d.ts","sourceRoot":"","sources":["../../../../../../src/core/tools/ask-user-question/view/components/chat-row-view.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAkB,KAAK,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEzG;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED,MAAM,WAAW,iBAAiB;IACjC,oFAAoF;IACpF,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,mBAAmB,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,qBAAa,WAAY,YAAW,YAAY,CAAC,gBAAgB,CAAC,EAAE,SAAS;IAC5E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IAExC,YAAY,MAAM,EAAE,iBAAiB,EAKpC;IAED,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAGtC;IAED,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAG;IAEnC,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9B;CACD","sourcesContent":["import type { Component } from \"@earendil-works/pi-tui\";\nimport type { StatefulView } from \"../stateful-view.ts\";\nimport { WrappingSelect, type WrappingSelectItem, type WrappingSelectTheme } from \"./wrapping-select.ts\";\n\n/**\n * Per-tick projection of chat-row state. The chat row is a single-item\n * `WrappingSelect` rendered in the question-tab footer; it owns no per-tab\n * state — only `focused` (whether the chat row is the active focus target)\n * and `numbering` (display number aligned with the active tab's items).\n */\nexport interface ChatRowViewProps {\n\tfocused: boolean;\n\tnumbering: { offset: number; total: number };\n}\n\nexport interface ChatRowViewConfig {\n\t/** The single chat sentinel row — `{kind: \"chat\", label: SENTINEL_LABELS.chat}`. */\n\titem: WrappingSelectItem;\n\ttheme: WrappingSelectTheme;\n}\n\n/**\n * Typed wrapper around the chat-row `WrappingSelect`. Replaces the prior\n * raw-primitive consumption at `props-adapter.ts:106, :120` and removes the\n * accidental surface area (8 unused `WrappingSelect` setters) noted in\n * research Q4.\n *\n * Pattern modeled after `OptionListView` (`option-list-view.ts:27-93`):\n * mirror-then-delegate `setProps`; render is pure delegation; `Component`\n * triplet forwards.\n */\nexport class ChatRowView implements StatefulView<ChatRowViewProps>, Component {\n\tprivate readonly select: WrappingSelect;\n\n\tconstructor(config: ChatRowViewConfig) {\n\t\tthis.select = new WrappingSelect([config.item], 1, config.theme, {\n\t\t\tnumberStartOffset: 0,\n\t\t\ttotalItemsForNumbering: 1,\n\t\t});\n\t}\n\n\tsetProps(props: ChatRowViewProps): void {\n\t\tthis.select.setFocused(props.focused);\n\t\tthis.select.setNumbering(props.numbering.offset, props.numbering.total);\n\t}\n\n\thandleInput(_data: string): void {}\n\n\tinvalidate(): void {\n\t\tthis.select.invalidate();\n\t}\n\n\trender(width: number): string[] {\n\t\treturn this.select.render(width);\n\t}\n}\n"]}
1
+ {"version":3,"file":"chat-row-view.d.ts","sourceRoot":"","sources":["../../../../../../src/core/tools/ask-user-question/view/components/chat-row-view.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAkB,KAAK,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEzG;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IACjC,oFAAoF;IACpF,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,mBAAmB,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,qBAAa,WAAY,YAAW,YAAY,CAAC,gBAAgB,CAAC,EAAE,SAAS;IAC5E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IAExC,YAAY,MAAM,EAAE,iBAAiB,EAKpC;IAED,QAAQ,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAKtC;IAED,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAG;IAEnC,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAE9B;CACD","sourcesContent":["import type { Component } from \"@earendil-works/pi-tui\";\nimport type { StatefulView } from \"../stateful-view.ts\";\nimport { WrappingSelect, type WrappingSelectItem, type WrappingSelectTheme } from \"./wrapping-select.ts\";\n\n/**\n * Per-tick projection of chat-row state. The chat row is a single-item\n * `WrappingSelect` rendered in the question-tab footer; it owns no per-tab\n * state — only `focused` (whether the chat row is the active focus target),\n * `numbering` (display number aligned with the active tab's items), and the\n * owner-specific inline input projection supplied by the session.\n */\nexport interface ChatRowViewProps {\n\tfocused: boolean;\n\tnumbering: { offset: number; total: number };\n\tinputBuffer: string;\n\tinputCaret: number;\n}\n\nexport interface ChatRowViewConfig {\n\t/** The single chat sentinel row — `{kind: \"chat\", label: SENTINEL_LABELS.chat}`. */\n\titem: WrappingSelectItem;\n\ttheme: WrappingSelectTheme;\n}\n\n/**\n * Typed wrapper around the chat-row `WrappingSelect`. Replaces the prior\n * raw-primitive consumption at `props-adapter.ts:106, :120` and removes the\n * accidental surface area (8 unused `WrappingSelect` setters) noted in\n * research Q4.\n *\n * Pattern modeled after `OptionListView` (`option-list-view.ts:27-93`):\n * mirror-then-delegate `setProps`; render is pure delegation; `Component`\n * triplet forwards.\n */\nexport class ChatRowView implements StatefulView<ChatRowViewProps>, Component {\n\tprivate readonly select: WrappingSelect;\n\n\tconstructor(config: ChatRowViewConfig) {\n\t\tthis.select = new WrappingSelect([config.item], 1, config.theme, {\n\t\t\tnumberStartOffset: 0,\n\t\t\ttotalItemsForNumbering: 1,\n\t\t});\n\t}\n\n\tsetProps(props: ChatRowViewProps): void {\n\t\tthis.select.setFocused(props.focused);\n\t\tthis.select.setNumbering(props.numbering.offset, props.numbering.total);\n\t\tthis.select.setInputBuffer(props.inputBuffer);\n\t\tthis.select.setInputCursor(props.inputCaret);\n\t}\n\n\thandleInput(_data: string): void {}\n\n\tinvalidate(): void {\n\t\tthis.select.invalidate();\n\t}\n\n\trender(width: number): string[] {\n\t\treturn this.select.render(width);\n\t}\n}\n"]}
@@ -19,6 +19,8 @@ export class ChatRowView {
19
19
  setProps(props) {
20
20
  this.select.setFocused(props.focused);
21
21
  this.select.setNumbering(props.numbering.offset, props.numbering.total);
22
+ this.select.setInputBuffer(props.inputBuffer);
23
+ this.select.setInputCursor(props.inputCaret);
22
24
  }
23
25
  handleInput(_data) { }
24
26
  invalidate() {
@@ -1 +1 @@
1
- {"version":3,"file":"chat-row-view.js","sourceRoot":"","sources":["../../../../../../src/core/tools/ask-user-question/view/components/chat-row-view.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAqD,MAAM,sBAAsB,CAAC;AAmBzG;;;;;;;;;GASG;AACH,MAAM,OAAO,WAAW;IAGvB,YAAY,MAAyB;QACpC,IAAI,CAAC,MAAM,GAAG,IAAI,cAAc,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE;YAChE,iBAAiB,EAAE,CAAC;YACpB,sBAAsB,EAAE,CAAC;SACzB,CAAC,CAAC;IACJ,CAAC;IAED,QAAQ,CAAC,KAAuB;QAC/B,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACzE,CAAC;IAED,WAAW,CAAC,KAAa,IAAS,CAAC;IAEnC,UAAU;QACT,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IAC1B,CAAC;IAED,MAAM,CAAC,KAAa;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;CACD","sourcesContent":["import type { Component } from \"@earendil-works/pi-tui\";\nimport type { StatefulView } from \"../stateful-view.ts\";\nimport { WrappingSelect, type WrappingSelectItem, type WrappingSelectTheme } from \"./wrapping-select.ts\";\n\n/**\n * Per-tick projection of chat-row state. The chat row is a single-item\n * `WrappingSelect` rendered in the question-tab footer; it owns no per-tab\n * state — only `focused` (whether the chat row is the active focus target)\n * and `numbering` (display number aligned with the active tab's items).\n */\nexport interface ChatRowViewProps {\n\tfocused: boolean;\n\tnumbering: { offset: number; total: number };\n}\n\nexport interface ChatRowViewConfig {\n\t/** The single chat sentinel row — `{kind: \"chat\", label: SENTINEL_LABELS.chat}`. */\n\titem: WrappingSelectItem;\n\ttheme: WrappingSelectTheme;\n}\n\n/**\n * Typed wrapper around the chat-row `WrappingSelect`. Replaces the prior\n * raw-primitive consumption at `props-adapter.ts:106, :120` and removes the\n * accidental surface area (8 unused `WrappingSelect` setters) noted in\n * research Q4.\n *\n * Pattern modeled after `OptionListView` (`option-list-view.ts:27-93`):\n * mirror-then-delegate `setProps`; render is pure delegation; `Component`\n * triplet forwards.\n */\nexport class ChatRowView implements StatefulView<ChatRowViewProps>, Component {\n\tprivate readonly select: WrappingSelect;\n\n\tconstructor(config: ChatRowViewConfig) {\n\t\tthis.select = new WrappingSelect([config.item], 1, config.theme, {\n\t\t\tnumberStartOffset: 0,\n\t\t\ttotalItemsForNumbering: 1,\n\t\t});\n\t}\n\n\tsetProps(props: ChatRowViewProps): void {\n\t\tthis.select.setFocused(props.focused);\n\t\tthis.select.setNumbering(props.numbering.offset, props.numbering.total);\n\t}\n\n\thandleInput(_data: string): void {}\n\n\tinvalidate(): void {\n\t\tthis.select.invalidate();\n\t}\n\n\trender(width: number): string[] {\n\t\treturn this.select.render(width);\n\t}\n}\n"]}
1
+ {"version":3,"file":"chat-row-view.js","sourceRoot":"","sources":["../../../../../../src/core/tools/ask-user-question/view/components/chat-row-view.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAqD,MAAM,sBAAsB,CAAC;AAsBzG;;;;;;;;;GASG;AACH,MAAM,OAAO,WAAW;IAGvB,YAAY,MAAyB;QACpC,IAAI,CAAC,MAAM,GAAG,IAAI,cAAc,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE;YAChE,iBAAiB,EAAE,CAAC;YACpB,sBAAsB,EAAE,CAAC;SACzB,CAAC,CAAC;IACJ,CAAC;IAED,QAAQ,CAAC,KAAuB;QAC/B,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACxE,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC9C,CAAC;IAED,WAAW,CAAC,KAAa,IAAS,CAAC;IAEnC,UAAU;QACT,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;IAC1B,CAAC;IAED,MAAM,CAAC,KAAa;QACnB,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;CACD","sourcesContent":["import type { Component } from \"@earendil-works/pi-tui\";\nimport type { StatefulView } from \"../stateful-view.ts\";\nimport { WrappingSelect, type WrappingSelectItem, type WrappingSelectTheme } from \"./wrapping-select.ts\";\n\n/**\n * Per-tick projection of chat-row state. The chat row is a single-item\n * `WrappingSelect` rendered in the question-tab footer; it owns no per-tab\n * state — only `focused` (whether the chat row is the active focus target),\n * `numbering` (display number aligned with the active tab's items), and the\n * owner-specific inline input projection supplied by the session.\n */\nexport interface ChatRowViewProps {\n\tfocused: boolean;\n\tnumbering: { offset: number; total: number };\n\tinputBuffer: string;\n\tinputCaret: number;\n}\n\nexport interface ChatRowViewConfig {\n\t/** The single chat sentinel row — `{kind: \"chat\", label: SENTINEL_LABELS.chat}`. */\n\titem: WrappingSelectItem;\n\ttheme: WrappingSelectTheme;\n}\n\n/**\n * Typed wrapper around the chat-row `WrappingSelect`. Replaces the prior\n * raw-primitive consumption at `props-adapter.ts:106, :120` and removes the\n * accidental surface area (8 unused `WrappingSelect` setters) noted in\n * research Q4.\n *\n * Pattern modeled after `OptionListView` (`option-list-view.ts:27-93`):\n * mirror-then-delegate `setProps`; render is pure delegation; `Component`\n * triplet forwards.\n */\nexport class ChatRowView implements StatefulView<ChatRowViewProps>, Component {\n\tprivate readonly select: WrappingSelect;\n\n\tconstructor(config: ChatRowViewConfig) {\n\t\tthis.select = new WrappingSelect([config.item], 1, config.theme, {\n\t\t\tnumberStartOffset: 0,\n\t\t\ttotalItemsForNumbering: 1,\n\t\t});\n\t}\n\n\tsetProps(props: ChatRowViewProps): void {\n\t\tthis.select.setFocused(props.focused);\n\t\tthis.select.setNumbering(props.numbering.offset, props.numbering.total);\n\t\tthis.select.setInputBuffer(props.inputBuffer);\n\t\tthis.select.setInputCursor(props.inputCaret);\n\t}\n\n\thandleInput(_data: string): void {}\n\n\tinvalidate(): void {\n\t\tthis.select.invalidate();\n\t}\n\n\trender(width: number): string[] {\n\t\treturn this.select.render(width);\n\t}\n}\n"]}
@@ -11,6 +11,7 @@ import type { Component } from "@earendil-works/pi-tui";
11
11
  * - `other`: the inline free-text input row appended to single-select questions
12
12
  * (label is "Type something."). Renders as inline `Input` when active.
13
13
  * - `chat`: the abandon-questionnaire escape-hatch row (label is "Chat about this").
14
+ * Renders as inline `Input` when active.
14
15
  * - `next`: the explicit commit-and-advance row appended to multi-select questions
15
16
  * (label is "Next"). Renders without a number / checkbox.
16
17
  */
@@ -1 +1 @@
1
- {"version":3,"file":"wrapping-select.d.ts","sourceRoot":"","sources":["../../../../../../src/core/tools/ask-user-question/view/components/wrapping-select.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"@earendil-works/pi-tui\";\nimport { visibleWidth, wrapTextWithAnsi } from \"@earendil-works/pi-tui\";\n\n/**\n * Row-intent discriminated union. `kind` is the single discriminator —\n * pre-1.0.3 boolean flags have been removed (see `banned-flags.test.ts`).\n * Modeled after `QuestionnaireAction` (`key-router.ts:13-32`) and `Effect`\n * (`state-reducer.ts:26-32`) — pure literal-tagged variants, no shared base,\n * exhaustive-`switch` enforcement via non-`void` returns.\n *\n * Variant semantics:\n * - `option`: a regular author-defined option row.\n * - `other`: the inline free-text input row appended to single-select questions\n * (label is \"Type something.\"). Renders as inline `Input` when active.\n * - `chat`: the abandon-questionnaire escape-hatch row (label is \"Chat about this\").\n * - `next`: the explicit commit-and-advance row appended to multi-select questions\n * (label is \"Next\"). Renders without a number / checkbox.\n */\nexport type WrappingSelectItem =\n\t| { kind: \"option\"; label: string; description?: string }\n\t| { kind: \"other\"; label: string; description?: string }\n\t| { kind: \"chat\"; label: string; description?: string }\n\t| { kind: \"next\"; label: string; description?: string };\n\nexport interface WrappingSelectTheme {\n\tselectedText: (text: string) => string;\n\tdescription: (text: string) => string;\n\tscrollInfo: (text: string) => string;\n}\n\n/**\n * Numbering controls.\n *\n * Use `numberStartOffset` + `totalItemsForNumbering` when a list is logically a slice of a\n * larger numbered sequence — e.g. the chat row lives in its own WrappingSelect but should\n * render as `(N+1).` where N is the previous list's item count, with the column padded as\n * if both lists were one continuous numbered sequence.\n */\nexport interface WrappingSelectOptions {\n\t/** Start numbering at this offset + 1 (default 0 → rows labeled 1, 2, 3 …). */\n\tnumberStartOffset?: number;\n\t/** Override the total used to pad the number column (useful when items span multiple lists). */\n\ttotalItemsForNumbering?: number;\n}\n\nexport class WrappingSelect implements Component {\n\tprivate static readonly ACTIVE_POINTER = \"❯ \";\n\tprivate static readonly INACTIVE_POINTER = \" \";\n\tprivate static readonly NUMBER_SEPARATOR = \". \";\n\tprivate static readonly CURSOR_INVERSE_START = \"\\x1b[7m\";\n\tprivate static readonly CURSOR_INVERSE_END = \"\\x1b[0m\";\n\tprivate static readonly CONFIRMED_MARK = \" ✔\";\n\tprivate static readonly MIN_CONTENT_WIDTH = 1;\n\n\tprivate readonly items: readonly WrappingSelectItem[];\n\tprivate readonly maxVisible: number;\n\tprivate readonly theme: WrappingSelectTheme;\n\tprivate numberStartOffset: number;\n\tprivate totalItemsForNumbering: number;\n\n\tprivate selectedIndex = 0;\n\tprivate focused = true;\n\tprivate inputBuffer = \"\";\n\tprivate inputCursor: number | undefined = undefined;\n\t/**\n\t * Index of the row that was previously confirmed for this list (e.g. the user's prior\n\t * answer when re-entering a multi-question tab). Renders `<label> ✔` in the active-row\n\t * styling but WITHOUT the `❯` pointer — pointer is reserved for the live cursor. When\n\t * `selectedIndex === confirmedIndex && focused`, the active rendering wins (no double-mark).\n\t */\n\tprivate confirmedIndex: number | undefined = undefined;\n\t/**\n\t * When set together with `confirmedIndex`, replaces the row's static label at render time.\n\t * Used for the `kind: \"other\"` sentinel — its label is \"Type something.\" but if the user's\n\t * prior answer was custom text, we render that text instead (e.g. `4. Hello ✔`).\n\t */\n\tprivate confirmedLabelOverride: string | undefined = undefined;\n\n\tconstructor(\n\t\titems: readonly WrappingSelectItem[],\n\t\tmaxVisible: number,\n\t\ttheme: WrappingSelectTheme,\n\t\toptions: WrappingSelectOptions = {},\n\t) {\n\t\tthis.items = items;\n\t\tthis.maxVisible = Math.max(1, maxVisible);\n\t\tthis.theme = theme;\n\t\tthis.numberStartOffset = options.numberStartOffset ?? 0;\n\t\tthis.totalItemsForNumbering = options.totalItemsForNumbering ?? items.length;\n\t}\n\n\t/**\n\t * Update the numbering offset + total padding width without rebuilding the component.\n\t * Used by the host to keep the chat-row WrappingSelect's number aligned with the active tab's\n\t * options list when the user switches tabs (each tab can have a different items count).\n\t */\n\tsetNumbering(numberStartOffset: number, totalItemsForNumbering: number): void {\n\t\tthis.numberStartOffset = numberStartOffset;\n\t\tthis.totalItemsForNumbering = Math.max(1, totalItemsForNumbering);\n\t}\n\n\tsetSelectedIndex(index: number): void {\n\t\tthis.selectedIndex = Math.max(0, Math.min(index, this.items.length - 1));\n\t}\n\n\tsetFocused(focused: boolean): void {\n\t\tthis.focused = focused;\n\t}\n\n\t/**\n\t * Mark a previously-confirmed row. Pass `undefined` to clear. `labelOverride` replaces\n\t * the row's static `item.label` at render time — used for the `kind: \"other\"` sentinel so\n\t * the row reads `Hello ✔` instead of `Type something. ✔` when the prior answer was custom\n\t * text.\n\t */\n\tsetConfirmedIndex(index: number | undefined, labelOverride?: string): void {\n\t\tif (index === undefined) {\n\t\t\tthis.confirmedIndex = undefined;\n\t\t\tthis.confirmedLabelOverride = undefined;\n\t\t\treturn;\n\t\t}\n\t\tthis.confirmedIndex = Math.max(0, Math.min(index, this.items.length - 1));\n\t\tthis.confirmedLabelOverride = labelOverride;\n\t}\n\n\tsetInputBuffer(text: string): void {\n\t\tthis.inputBuffer = text;\n\t\tif (this.inputCursor !== undefined) this.inputCursor = this.clampInputCursor(this.inputCursor);\n\t}\n\n\tsetInputCursor(cursor: number): void {\n\t\tthis.inputCursor = this.clampInputCursor(cursor);\n\t}\n\n\t/** Intentionally empty — input is routed at the container level. */\n\thandleInput(_data: string): void {}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tif (this.items.length === 0) return [];\n\n\t\tconst { startIndex, endIndex } = this.computeVisibleWindow();\n\t\tconst numberWidth = String(Math.max(1, this.totalItemsForNumbering)).length;\n\t\tconst lines: string[] = [];\n\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.items[i];\n\t\t\tif (!item) continue;\n\t\t\tconst isActive = i === this.selectedIndex && this.focused;\n\t\t\tlines.push(...this.renderItem(item, i, isActive, width, numberWidth));\n\t\t}\n\n\t\tif (this.hasItemsOutsideWindow(startIndex, endIndex)) {\n\t\t\tlines.push(this.theme.scrollInfo(` (${this.selectedIndex + 1}/${this.items.length})`));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate computeVisibleWindow(): { startIndex: number; endIndex: number } {\n\t\tconst half = Math.floor(this.maxVisible / 2);\n\t\tconst startIndex = Math.max(0, Math.min(this.selectedIndex - half, this.items.length - this.maxVisible));\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.items.length);\n\t\treturn { startIndex, endIndex };\n\t}\n\n\tprivate hasItemsOutsideWindow(startIndex: number, endIndex: number): boolean {\n\t\treturn startIndex > 0 || endIndex < this.items.length;\n\t}\n\n\tprivate renderItem(\n\t\titem: WrappingSelectItem,\n\t\tindex: number,\n\t\tisActive: boolean,\n\t\twidth: number,\n\t\tnumberWidth: number,\n\t): string[] {\n\t\tconst rowPrefix = this.buildRowPrefix(index, isActive, numberWidth);\n\t\tconst continuationPrefix = \" \".repeat(visibleWidth(rowPrefix));\n\t\tconst contentWidth = Math.max(WrappingSelect.MIN_CONTENT_WIDTH, width - visibleWidth(rowPrefix));\n\n\t\tif (this.shouldRenderAsInlineInput(item, isActive)) {\n\t\t\treturn this.renderInlineInputRow(rowPrefix, continuationPrefix, contentWidth);\n\t\t}\n\n\t\t// Confirmed row gets a trailing ` ✔` and accent+bold styling; pointer is independent\n\t\t// (still ❯ when active). When `index === confirmedIndex` AND `isActive`, both `❯` and\n\t\t// `✔` appear on the same row — load-bearing for the case where the prior answer was\n\t\t// row 0 (cursor resets to 0 on tab-back, so the confirmed row IS the active row).\n\t\t// Optional `confirmedLabelOverride` replaces the static label (used for `kind: \"other\"`\n\t\t// + `kind: \"custom\"` answer); the inline-input branch above still wins for `kind: \"other\" + isActive`.\n\t\tconst isConfirmed = index === this.confirmedIndex;\n\t\tconst label = isConfirmed\n\t\t\t? `${this.confirmedLabelOverride ?? item.label}${WrappingSelect.CONFIRMED_MARK}`\n\t\t\t: item.label;\n\t\tconst applySelectedStyle = isActive || isConfirmed;\n\n\t\treturn [\n\t\t\t...this.renderLabelBlock(label, rowPrefix, continuationPrefix, contentWidth, applySelectedStyle),\n\t\t\t...this.renderDescriptionBlock(item.description, continuationPrefix, contentWidth),\n\t\t];\n\t}\n\n\tprivate buildRowPrefix(index: number, isActive: boolean, numberWidth: number): string {\n\t\tconst pointer = isActive ? WrappingSelect.ACTIVE_POINTER : WrappingSelect.INACTIVE_POINTER;\n\t\tconst displayNumber = this.numberStartOffset + index + 1;\n\t\tconst paddedNumber = String(displayNumber).padStart(numberWidth, \" \");\n\t\treturn `${pointer}${paddedNumber}${WrappingSelect.NUMBER_SEPARATOR}`;\n\t}\n\n\tprivate shouldRenderAsInlineInput(item: WrappingSelectItem, isActive: boolean): boolean {\n\t\treturn item.kind === \"other\" && isActive;\n\t}\n\n\t/**\n\t * Render the inline input row across one or more lines, wrapping at `contentWidth`\n\t * so long input doesn't run off the right edge or trip the parent renderer's\n\t * width invariant. Mirrors `renderLabelBlock`'s contract: first line carries\n\t * `rowPrefix`, continuation lines carry `continuationPrefix` (spaces), and every\n\t * emitted line passes through `theme.selectedText`. The cursor mirrors the\n\t * main chat editor: inverse-video over the character at the caret, or an\n\t * inverse-video space at end-of-line.\n\t */\n\tprivate renderInlineInputRow(rowPrefix: string, continuationPrefix: string, contentWidth: number): string[] {\n\t\tconst raw = this.inputTextWithCursor();\n\t\tconst wrapped = wrapTextWithAnsi(raw, contentWidth);\n\t\treturn wrapped.map((segment, index) => {\n\t\t\tconst prefix = index === 0 ? rowPrefix : continuationPrefix;\n\t\t\treturn this.theme.selectedText(`${prefix}${segment}`);\n\t\t});\n\t}\n\n\tprivate inputTextWithCursor(): string {\n\t\tconst cursor = this.clampInputCursor(this.inputCursor ?? this.inputBuffer.length);\n\t\tconst before = this.inputBuffer.slice(0, cursor);\n\t\tconst after = this.inputBuffer.slice(cursor);\n\t\tconst [at = \"\"] = Array.from(after);\n\t\tif (at === \"\") {\n\t\t\treturn `${before}${WrappingSelect.CURSOR_INVERSE_START} ${WrappingSelect.CURSOR_INVERSE_END}`;\n\t\t}\n\t\tconst rest = after.slice(at.length);\n\t\treturn `${before}${WrappingSelect.CURSOR_INVERSE_START}${at}${WrappingSelect.CURSOR_INVERSE_END}${rest}`;\n\t}\n\n\tprivate clampInputCursor(cursor: number): number {\n\t\tif (!Number.isFinite(cursor)) return this.inputBuffer.length;\n\t\treturn Math.max(0, Math.min(cursor, this.inputBuffer.length));\n\t}\n\n\tprivate renderLabelBlock(\n\t\tlabel: string,\n\t\trowPrefix: string,\n\t\tcontinuationPrefix: string,\n\t\tcontentWidth: number,\n\t\tapplySelectedStyle: boolean,\n\t): string[] {\n\t\tconst wrapped = wrapTextWithAnsi(label, contentWidth);\n\t\treturn wrapped.map((segment, index) => {\n\t\t\tconst prefix = index === 0 ? rowPrefix : continuationPrefix;\n\t\t\tconst line = `${prefix}${segment}`;\n\t\t\treturn applySelectedStyle ? this.theme.selectedText(line) : line;\n\t\t});\n\t}\n\n\tprivate renderDescriptionBlock(\n\t\tdescription: string | undefined,\n\t\tcontinuationPrefix: string,\n\t\tcontentWidth: number,\n\t): string[] {\n\t\tif (!description) return [];\n\t\tconst wrapped = wrapTextWithAnsi(description, contentWidth);\n\t\treturn wrapped.map((segment) => `${continuationPrefix}${this.theme.description(segment)}`);\n\t}\n}\n"]}
1
+ {"version":3,"file":"wrapping-select.d.ts","sourceRoot":"","sources":["../../../../../../src/core/tools/ask-user-question/view/components/wrapping-select.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"@earendil-works/pi-tui\";\nimport { visibleWidth, wrapTextWithAnsi } from \"@earendil-works/pi-tui\";\nimport { ROW_INTENT_META } from \"../../state/row-intent.ts\";\n\n/**\n * Row-intent discriminated union. `kind` is the single discriminator —\n * pre-1.0.3 boolean flags have been removed (see `banned-flags.test.ts`).\n * Modeled after `QuestionnaireAction` (`key-router.ts:13-32`) and `Effect`\n * (`state-reducer.ts:26-32`) — pure literal-tagged variants, no shared base,\n * exhaustive-`switch` enforcement via non-`void` returns.\n *\n * Variant semantics:\n * - `option`: a regular author-defined option row.\n * - `other`: the inline free-text input row appended to single-select questions\n * (label is \"Type something.\"). Renders as inline `Input` when active.\n * - `chat`: the abandon-questionnaire escape-hatch row (label is \"Chat about this\").\n * Renders as inline `Input` when active.\n * - `next`: the explicit commit-and-advance row appended to multi-select questions\n * (label is \"Next\"). Renders without a number / checkbox.\n */\nexport type WrappingSelectItem =\n\t| { kind: \"option\"; label: string; description?: string }\n\t| { kind: \"other\"; label: string; description?: string }\n\t| { kind: \"chat\"; label: string; description?: string }\n\t| { kind: \"next\"; label: string; description?: string };\n\nexport interface WrappingSelectTheme {\n\tselectedText: (text: string) => string;\n\tdescription: (text: string) => string;\n\tscrollInfo: (text: string) => string;\n}\n\n/**\n * Numbering controls.\n *\n * Use `numberStartOffset` + `totalItemsForNumbering` when a list is logically a slice of a\n * larger numbered sequence — e.g. the chat row lives in its own WrappingSelect but should\n * render as `(N+1).` where N is the previous list's item count, with the column padded as\n * if both lists were one continuous numbered sequence.\n */\nexport interface WrappingSelectOptions {\n\t/** Start numbering at this offset + 1 (default 0 → rows labeled 1, 2, 3 …). */\n\tnumberStartOffset?: number;\n\t/** Override the total used to pad the number column (useful when items span multiple lists). */\n\ttotalItemsForNumbering?: number;\n}\n\nexport class WrappingSelect implements Component {\n\tprivate static readonly ACTIVE_POINTER = \"❯ \";\n\tprivate static readonly INACTIVE_POINTER = \" \";\n\tprivate static readonly NUMBER_SEPARATOR = \". \";\n\tprivate static readonly CURSOR_INVERSE_START = \"\\x1b[7m\";\n\tprivate static readonly CURSOR_INVERSE_END = \"\\x1b[0m\";\n\tprivate static readonly CONFIRMED_MARK = \" ✔\";\n\tprivate static readonly MIN_CONTENT_WIDTH = 1;\n\n\tprivate readonly items: readonly WrappingSelectItem[];\n\tprivate readonly maxVisible: number;\n\tprivate readonly theme: WrappingSelectTheme;\n\tprivate numberStartOffset: number;\n\tprivate totalItemsForNumbering: number;\n\n\tprivate selectedIndex = 0;\n\tprivate focused = true;\n\tprivate inputBuffer = \"\";\n\tprivate inputCursor: number | undefined = undefined;\n\t/**\n\t * Index of the row that was previously confirmed for this list (e.g. the user's prior\n\t * answer when re-entering a multi-question tab). Renders `<label> ✔` in the active-row\n\t * styling but WITHOUT the `❯` pointer — pointer is reserved for the live cursor. When\n\t * `selectedIndex === confirmedIndex && focused`, the active rendering wins (no double-mark).\n\t */\n\tprivate confirmedIndex: number | undefined = undefined;\n\t/**\n\t * When set together with `confirmedIndex`, replaces the row's static label at render time.\n\t * Used for the `kind: \"other\"` sentinel — its label is \"Type something.\" but if the user's\n\t * prior answer was custom text, we render that text instead (e.g. `4. Hello ✔`).\n\t */\n\tprivate confirmedLabelOverride: string | undefined = undefined;\n\n\tconstructor(\n\t\titems: readonly WrappingSelectItem[],\n\t\tmaxVisible: number,\n\t\ttheme: WrappingSelectTheme,\n\t\toptions: WrappingSelectOptions = {},\n\t) {\n\t\tthis.items = items;\n\t\tthis.maxVisible = Math.max(1, maxVisible);\n\t\tthis.theme = theme;\n\t\tthis.numberStartOffset = options.numberStartOffset ?? 0;\n\t\tthis.totalItemsForNumbering = options.totalItemsForNumbering ?? items.length;\n\t}\n\n\t/**\n\t * Update the numbering offset + total padding width without rebuilding the component.\n\t * Used by the host to keep the chat-row WrappingSelect's number aligned with the active tab's\n\t * options list when the user switches tabs (each tab can have a different items count).\n\t */\n\tsetNumbering(numberStartOffset: number, totalItemsForNumbering: number): void {\n\t\tthis.numberStartOffset = numberStartOffset;\n\t\tthis.totalItemsForNumbering = Math.max(1, totalItemsForNumbering);\n\t}\n\n\tsetSelectedIndex(index: number): void {\n\t\tthis.selectedIndex = Math.max(0, Math.min(index, this.items.length - 1));\n\t}\n\n\tsetFocused(focused: boolean): void {\n\t\tthis.focused = focused;\n\t}\n\n\t/**\n\t * Mark a previously-confirmed row. Pass `undefined` to clear. `labelOverride` replaces\n\t * the row's static `item.label` at render time — used for the `kind: \"other\"` sentinel so\n\t * the row reads `Hello ✔` instead of `Type something. ✔` when the prior answer was custom\n\t * text.\n\t */\n\tsetConfirmedIndex(index: number | undefined, labelOverride?: string): void {\n\t\tif (index === undefined) {\n\t\t\tthis.confirmedIndex = undefined;\n\t\t\tthis.confirmedLabelOverride = undefined;\n\t\t\treturn;\n\t\t}\n\t\tthis.confirmedIndex = Math.max(0, Math.min(index, this.items.length - 1));\n\t\tthis.confirmedLabelOverride = labelOverride;\n\t}\n\n\tsetInputBuffer(text: string): void {\n\t\tthis.inputBuffer = text;\n\t\tif (this.inputCursor !== undefined) this.inputCursor = this.clampInputCursor(this.inputCursor);\n\t}\n\n\tsetInputCursor(cursor: number): void {\n\t\tthis.inputCursor = this.clampInputCursor(cursor);\n\t}\n\n\t/** Intentionally empty — input is routed at the container level. */\n\thandleInput(_data: string): void {}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tif (this.items.length === 0) return [];\n\n\t\tconst { startIndex, endIndex } = this.computeVisibleWindow();\n\t\tconst numberWidth = String(Math.max(1, this.totalItemsForNumbering)).length;\n\t\tconst lines: string[] = [];\n\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.items[i];\n\t\t\tif (!item) continue;\n\t\t\tconst isActive = i === this.selectedIndex && this.focused;\n\t\t\tlines.push(...this.renderItem(item, i, isActive, width, numberWidth));\n\t\t}\n\n\t\tif (this.hasItemsOutsideWindow(startIndex, endIndex)) {\n\t\t\tlines.push(this.theme.scrollInfo(` (${this.selectedIndex + 1}/${this.items.length})`));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate computeVisibleWindow(): { startIndex: number; endIndex: number } {\n\t\tconst half = Math.floor(this.maxVisible / 2);\n\t\tconst startIndex = Math.max(0, Math.min(this.selectedIndex - half, this.items.length - this.maxVisible));\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.items.length);\n\t\treturn { startIndex, endIndex };\n\t}\n\n\tprivate hasItemsOutsideWindow(startIndex: number, endIndex: number): boolean {\n\t\treturn startIndex > 0 || endIndex < this.items.length;\n\t}\n\n\tprivate renderItem(\n\t\titem: WrappingSelectItem,\n\t\tindex: number,\n\t\tisActive: boolean,\n\t\twidth: number,\n\t\tnumberWidth: number,\n\t): string[] {\n\t\tconst rowPrefix = this.buildRowPrefix(index, isActive, numberWidth);\n\t\tconst continuationPrefix = \" \".repeat(visibleWidth(rowPrefix));\n\t\tconst contentWidth = Math.max(WrappingSelect.MIN_CONTENT_WIDTH, width - visibleWidth(rowPrefix));\n\n\t\tif (this.shouldRenderAsInlineInput(item, isActive)) {\n\t\t\treturn this.renderInlineInputRow(rowPrefix, continuationPrefix, contentWidth);\n\t\t}\n\n\t\t// Confirmed row gets a trailing ` ✔` and accent+bold styling; pointer is independent\n\t\t// (still ❯ when active). When `index === confirmedIndex` AND `isActive`, both `❯` and\n\t\t// `✔` appear on the same row — load-bearing for the case where the prior answer was\n\t\t// row 0 (cursor resets to 0 on tab-back, so the confirmed row IS the active row).\n\t\t// Optional `confirmedLabelOverride` replaces the static label (used for `kind: \"other\"`\n\t\t// + `kind: \"custom\"` answer); the inline-input branch above still wins for `kind: \"other\" + isActive`.\n\t\tconst isConfirmed = index === this.confirmedIndex;\n\t\tconst label = isConfirmed\n\t\t\t? `${this.confirmedLabelOverride ?? item.label}${WrappingSelect.CONFIRMED_MARK}`\n\t\t\t: item.label;\n\t\tconst applySelectedStyle = isActive || isConfirmed;\n\n\t\treturn [\n\t\t\t...this.renderLabelBlock(label, rowPrefix, continuationPrefix, contentWidth, applySelectedStyle),\n\t\t\t...this.renderDescriptionBlock(item.description, continuationPrefix, contentWidth),\n\t\t];\n\t}\n\n\tprivate buildRowPrefix(index: number, isActive: boolean, numberWidth: number): string {\n\t\tconst pointer = isActive ? WrappingSelect.ACTIVE_POINTER : WrappingSelect.INACTIVE_POINTER;\n\t\tconst displayNumber = this.numberStartOffset + index + 1;\n\t\tconst paddedNumber = String(displayNumber).padStart(numberWidth, \" \");\n\t\treturn `${pointer}${paddedNumber}${WrappingSelect.NUMBER_SEPARATOR}`;\n\t}\n\n\tprivate shouldRenderAsInlineInput(item: WrappingSelectItem, isActive: boolean): boolean {\n\t\treturn isActive && ROW_INTENT_META[item.kind].activatesInputMode;\n\t}\n\n\t/**\n\t * Render the inline input row across one or more lines, wrapping at `contentWidth`\n\t * so long input doesn't run off the right edge or trip the parent renderer's\n\t * width invariant. Mirrors `renderLabelBlock`'s contract: first line carries\n\t * `rowPrefix`, continuation lines carry `continuationPrefix` (spaces), and every\n\t * emitted line passes through `theme.selectedText`. The cursor mirrors the\n\t * main chat editor: inverse-video over the character at the caret, or an\n\t * inverse-video space at end-of-line.\n\t */\n\tprivate renderInlineInputRow(rowPrefix: string, continuationPrefix: string, contentWidth: number): string[] {\n\t\tconst raw = this.inputTextWithCursor();\n\t\tconst wrapped = wrapTextWithAnsi(raw, contentWidth);\n\t\treturn wrapped.map((segment, index) => {\n\t\t\tconst prefix = index === 0 ? rowPrefix : continuationPrefix;\n\t\t\treturn this.theme.selectedText(`${prefix}${segment}`);\n\t\t});\n\t}\n\n\tprivate inputTextWithCursor(): string {\n\t\tconst cursor = this.clampInputCursor(this.inputCursor ?? this.inputBuffer.length);\n\t\tconst before = this.inputBuffer.slice(0, cursor);\n\t\tconst after = this.inputBuffer.slice(cursor);\n\t\tconst [at = \"\"] = Array.from(after);\n\t\tif (at === \"\") {\n\t\t\treturn `${before}${WrappingSelect.CURSOR_INVERSE_START} ${WrappingSelect.CURSOR_INVERSE_END}`;\n\t\t}\n\t\tconst rest = after.slice(at.length);\n\t\treturn `${before}${WrappingSelect.CURSOR_INVERSE_START}${at}${WrappingSelect.CURSOR_INVERSE_END}${rest}`;\n\t}\n\n\tprivate clampInputCursor(cursor: number): number {\n\t\tif (!Number.isFinite(cursor)) return this.inputBuffer.length;\n\t\treturn Math.max(0, Math.min(cursor, this.inputBuffer.length));\n\t}\n\n\tprivate renderLabelBlock(\n\t\tlabel: string,\n\t\trowPrefix: string,\n\t\tcontinuationPrefix: string,\n\t\tcontentWidth: number,\n\t\tapplySelectedStyle: boolean,\n\t): string[] {\n\t\tconst wrapped = wrapTextWithAnsi(label, contentWidth);\n\t\treturn wrapped.map((segment, index) => {\n\t\t\tconst prefix = index === 0 ? rowPrefix : continuationPrefix;\n\t\t\tconst line = `${prefix}${segment}`;\n\t\t\treturn applySelectedStyle ? this.theme.selectedText(line) : line;\n\t\t});\n\t}\n\n\tprivate renderDescriptionBlock(\n\t\tdescription: string | undefined,\n\t\tcontinuationPrefix: string,\n\t\tcontentWidth: number,\n\t): string[] {\n\t\tif (!description) return [];\n\t\tconst wrapped = wrapTextWithAnsi(description, contentWidth);\n\t\treturn wrapped.map((segment) => `${continuationPrefix}${this.theme.description(segment)}`);\n\t}\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ import { ROW_INTENT_META } from "../../state/row-intent.js";
2
3
  export class WrappingSelect {
3
4
  static { this.ACTIVE_POINTER = "❯ "; }
4
5
  static { this.INACTIVE_POINTER = " "; }
@@ -129,7 +130,7 @@ export class WrappingSelect {
129
130
  return `${pointer}${paddedNumber}${WrappingSelect.NUMBER_SEPARATOR}`;
130
131
  }
131
132
  shouldRenderAsInlineInput(item, isActive) {
132
- return item.kind === "other" && isActive;
133
+ return isActive && ROW_INTENT_META[item.kind].activatesInputMode;
133
134
  }
134
135
  /**
135
136
  * Render the inline input row across one or more lines, wrapping at `contentWidth`
@@ -1 +1 @@
1
- {"version":3,"file":"wrapping-select.js","sourceRoot":"","sources":["../../../../../../src/core/tools/ask-user-question/view/components/wrapping-select.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"@earendil-works/pi-tui\";\nimport { visibleWidth, wrapTextWithAnsi } from \"@earendil-works/pi-tui\";\n\n/**\n * Row-intent discriminated union. `kind` is the single discriminator —\n * pre-1.0.3 boolean flags have been removed (see `banned-flags.test.ts`).\n * Modeled after `QuestionnaireAction` (`key-router.ts:13-32`) and `Effect`\n * (`state-reducer.ts:26-32`) — pure literal-tagged variants, no shared base,\n * exhaustive-`switch` enforcement via non-`void` returns.\n *\n * Variant semantics:\n * - `option`: a regular author-defined option row.\n * - `other`: the inline free-text input row appended to single-select questions\n * (label is \"Type something.\"). Renders as inline `Input` when active.\n * - `chat`: the abandon-questionnaire escape-hatch row (label is \"Chat about this\").\n * - `next`: the explicit commit-and-advance row appended to multi-select questions\n * (label is \"Next\"). Renders without a number / checkbox.\n */\nexport type WrappingSelectItem =\n\t| { kind: \"option\"; label: string; description?: string }\n\t| { kind: \"other\"; label: string; description?: string }\n\t| { kind: \"chat\"; label: string; description?: string }\n\t| { kind: \"next\"; label: string; description?: string };\n\nexport interface WrappingSelectTheme {\n\tselectedText: (text: string) => string;\n\tdescription: (text: string) => string;\n\tscrollInfo: (text: string) => string;\n}\n\n/**\n * Numbering controls.\n *\n * Use `numberStartOffset` + `totalItemsForNumbering` when a list is logically a slice of a\n * larger numbered sequence — e.g. the chat row lives in its own WrappingSelect but should\n * render as `(N+1).` where N is the previous list's item count, with the column padded as\n * if both lists were one continuous numbered sequence.\n */\nexport interface WrappingSelectOptions {\n\t/** Start numbering at this offset + 1 (default 0 → rows labeled 1, 2, 3 …). */\n\tnumberStartOffset?: number;\n\t/** Override the total used to pad the number column (useful when items span multiple lists). */\n\ttotalItemsForNumbering?: number;\n}\n\nexport class WrappingSelect implements Component {\n\tprivate static readonly ACTIVE_POINTER = \"❯ \";\n\tprivate static readonly INACTIVE_POINTER = \" \";\n\tprivate static readonly NUMBER_SEPARATOR = \". \";\n\tprivate static readonly CURSOR_INVERSE_START = \"\\x1b[7m\";\n\tprivate static readonly CURSOR_INVERSE_END = \"\\x1b[0m\";\n\tprivate static readonly CONFIRMED_MARK = \" ✔\";\n\tprivate static readonly MIN_CONTENT_WIDTH = 1;\n\n\tprivate readonly items: readonly WrappingSelectItem[];\n\tprivate readonly maxVisible: number;\n\tprivate readonly theme: WrappingSelectTheme;\n\tprivate numberStartOffset: number;\n\tprivate totalItemsForNumbering: number;\n\n\tprivate selectedIndex = 0;\n\tprivate focused = true;\n\tprivate inputBuffer = \"\";\n\tprivate inputCursor: number | undefined = undefined;\n\t/**\n\t * Index of the row that was previously confirmed for this list (e.g. the user's prior\n\t * answer when re-entering a multi-question tab). Renders `<label> ✔` in the active-row\n\t * styling but WITHOUT the `❯` pointer — pointer is reserved for the live cursor. When\n\t * `selectedIndex === confirmedIndex && focused`, the active rendering wins (no double-mark).\n\t */\n\tprivate confirmedIndex: number | undefined = undefined;\n\t/**\n\t * When set together with `confirmedIndex`, replaces the row's static label at render time.\n\t * Used for the `kind: \"other\"` sentinel — its label is \"Type something.\" but if the user's\n\t * prior answer was custom text, we render that text instead (e.g. `4. Hello ✔`).\n\t */\n\tprivate confirmedLabelOverride: string | undefined = undefined;\n\n\tconstructor(\n\t\titems: readonly WrappingSelectItem[],\n\t\tmaxVisible: number,\n\t\ttheme: WrappingSelectTheme,\n\t\toptions: WrappingSelectOptions = {},\n\t) {\n\t\tthis.items = items;\n\t\tthis.maxVisible = Math.max(1, maxVisible);\n\t\tthis.theme = theme;\n\t\tthis.numberStartOffset = options.numberStartOffset ?? 0;\n\t\tthis.totalItemsForNumbering = options.totalItemsForNumbering ?? items.length;\n\t}\n\n\t/**\n\t * Update the numbering offset + total padding width without rebuilding the component.\n\t * Used by the host to keep the chat-row WrappingSelect's number aligned with the active tab's\n\t * options list when the user switches tabs (each tab can have a different items count).\n\t */\n\tsetNumbering(numberStartOffset: number, totalItemsForNumbering: number): void {\n\t\tthis.numberStartOffset = numberStartOffset;\n\t\tthis.totalItemsForNumbering = Math.max(1, totalItemsForNumbering);\n\t}\n\n\tsetSelectedIndex(index: number): void {\n\t\tthis.selectedIndex = Math.max(0, Math.min(index, this.items.length - 1));\n\t}\n\n\tsetFocused(focused: boolean): void {\n\t\tthis.focused = focused;\n\t}\n\n\t/**\n\t * Mark a previously-confirmed row. Pass `undefined` to clear. `labelOverride` replaces\n\t * the row's static `item.label` at render time — used for the `kind: \"other\"` sentinel so\n\t * the row reads `Hello ✔` instead of `Type something. ✔` when the prior answer was custom\n\t * text.\n\t */\n\tsetConfirmedIndex(index: number | undefined, labelOverride?: string): void {\n\t\tif (index === undefined) {\n\t\t\tthis.confirmedIndex = undefined;\n\t\t\tthis.confirmedLabelOverride = undefined;\n\t\t\treturn;\n\t\t}\n\t\tthis.confirmedIndex = Math.max(0, Math.min(index, this.items.length - 1));\n\t\tthis.confirmedLabelOverride = labelOverride;\n\t}\n\n\tsetInputBuffer(text: string): void {\n\t\tthis.inputBuffer = text;\n\t\tif (this.inputCursor !== undefined) this.inputCursor = this.clampInputCursor(this.inputCursor);\n\t}\n\n\tsetInputCursor(cursor: number): void {\n\t\tthis.inputCursor = this.clampInputCursor(cursor);\n\t}\n\n\t/** Intentionally empty — input is routed at the container level. */\n\thandleInput(_data: string): void {}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tif (this.items.length === 0) return [];\n\n\t\tconst { startIndex, endIndex } = this.computeVisibleWindow();\n\t\tconst numberWidth = String(Math.max(1, this.totalItemsForNumbering)).length;\n\t\tconst lines: string[] = [];\n\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.items[i];\n\t\t\tif (!item) continue;\n\t\t\tconst isActive = i === this.selectedIndex && this.focused;\n\t\t\tlines.push(...this.renderItem(item, i, isActive, width, numberWidth));\n\t\t}\n\n\t\tif (this.hasItemsOutsideWindow(startIndex, endIndex)) {\n\t\t\tlines.push(this.theme.scrollInfo(` (${this.selectedIndex + 1}/${this.items.length})`));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate computeVisibleWindow(): { startIndex: number; endIndex: number } {\n\t\tconst half = Math.floor(this.maxVisible / 2);\n\t\tconst startIndex = Math.max(0, Math.min(this.selectedIndex - half, this.items.length - this.maxVisible));\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.items.length);\n\t\treturn { startIndex, endIndex };\n\t}\n\n\tprivate hasItemsOutsideWindow(startIndex: number, endIndex: number): boolean {\n\t\treturn startIndex > 0 || endIndex < this.items.length;\n\t}\n\n\tprivate renderItem(\n\t\titem: WrappingSelectItem,\n\t\tindex: number,\n\t\tisActive: boolean,\n\t\twidth: number,\n\t\tnumberWidth: number,\n\t): string[] {\n\t\tconst rowPrefix = this.buildRowPrefix(index, isActive, numberWidth);\n\t\tconst continuationPrefix = \" \".repeat(visibleWidth(rowPrefix));\n\t\tconst contentWidth = Math.max(WrappingSelect.MIN_CONTENT_WIDTH, width - visibleWidth(rowPrefix));\n\n\t\tif (this.shouldRenderAsInlineInput(item, isActive)) {\n\t\t\treturn this.renderInlineInputRow(rowPrefix, continuationPrefix, contentWidth);\n\t\t}\n\n\t\t// Confirmed row gets a trailing ` ✔` and accent+bold styling; pointer is independent\n\t\t// (still ❯ when active). When `index === confirmedIndex` AND `isActive`, both `❯` and\n\t\t// `✔` appear on the same row — load-bearing for the case where the prior answer was\n\t\t// row 0 (cursor resets to 0 on tab-back, so the confirmed row IS the active row).\n\t\t// Optional `confirmedLabelOverride` replaces the static label (used for `kind: \"other\"`\n\t\t// + `kind: \"custom\"` answer); the inline-input branch above still wins for `kind: \"other\" + isActive`.\n\t\tconst isConfirmed = index === this.confirmedIndex;\n\t\tconst label = isConfirmed\n\t\t\t? `${this.confirmedLabelOverride ?? item.label}${WrappingSelect.CONFIRMED_MARK}`\n\t\t\t: item.label;\n\t\tconst applySelectedStyle = isActive || isConfirmed;\n\n\t\treturn [\n\t\t\t...this.renderLabelBlock(label, rowPrefix, continuationPrefix, contentWidth, applySelectedStyle),\n\t\t\t...this.renderDescriptionBlock(item.description, continuationPrefix, contentWidth),\n\t\t];\n\t}\n\n\tprivate buildRowPrefix(index: number, isActive: boolean, numberWidth: number): string {\n\t\tconst pointer = isActive ? WrappingSelect.ACTIVE_POINTER : WrappingSelect.INACTIVE_POINTER;\n\t\tconst displayNumber = this.numberStartOffset + index + 1;\n\t\tconst paddedNumber = String(displayNumber).padStart(numberWidth, \" \");\n\t\treturn `${pointer}${paddedNumber}${WrappingSelect.NUMBER_SEPARATOR}`;\n\t}\n\n\tprivate shouldRenderAsInlineInput(item: WrappingSelectItem, isActive: boolean): boolean {\n\t\treturn item.kind === \"other\" && isActive;\n\t}\n\n\t/**\n\t * Render the inline input row across one or more lines, wrapping at `contentWidth`\n\t * so long input doesn't run off the right edge or trip the parent renderer's\n\t * width invariant. Mirrors `renderLabelBlock`'s contract: first line carries\n\t * `rowPrefix`, continuation lines carry `continuationPrefix` (spaces), and every\n\t * emitted line passes through `theme.selectedText`. The cursor mirrors the\n\t * main chat editor: inverse-video over the character at the caret, or an\n\t * inverse-video space at end-of-line.\n\t */\n\tprivate renderInlineInputRow(rowPrefix: string, continuationPrefix: string, contentWidth: number): string[] {\n\t\tconst raw = this.inputTextWithCursor();\n\t\tconst wrapped = wrapTextWithAnsi(raw, contentWidth);\n\t\treturn wrapped.map((segment, index) => {\n\t\t\tconst prefix = index === 0 ? rowPrefix : continuationPrefix;\n\t\t\treturn this.theme.selectedText(`${prefix}${segment}`);\n\t\t});\n\t}\n\n\tprivate inputTextWithCursor(): string {\n\t\tconst cursor = this.clampInputCursor(this.inputCursor ?? this.inputBuffer.length);\n\t\tconst before = this.inputBuffer.slice(0, cursor);\n\t\tconst after = this.inputBuffer.slice(cursor);\n\t\tconst [at = \"\"] = Array.from(after);\n\t\tif (at === \"\") {\n\t\t\treturn `${before}${WrappingSelect.CURSOR_INVERSE_START} ${WrappingSelect.CURSOR_INVERSE_END}`;\n\t\t}\n\t\tconst rest = after.slice(at.length);\n\t\treturn `${before}${WrappingSelect.CURSOR_INVERSE_START}${at}${WrappingSelect.CURSOR_INVERSE_END}${rest}`;\n\t}\n\n\tprivate clampInputCursor(cursor: number): number {\n\t\tif (!Number.isFinite(cursor)) return this.inputBuffer.length;\n\t\treturn Math.max(0, Math.min(cursor, this.inputBuffer.length));\n\t}\n\n\tprivate renderLabelBlock(\n\t\tlabel: string,\n\t\trowPrefix: string,\n\t\tcontinuationPrefix: string,\n\t\tcontentWidth: number,\n\t\tapplySelectedStyle: boolean,\n\t): string[] {\n\t\tconst wrapped = wrapTextWithAnsi(label, contentWidth);\n\t\treturn wrapped.map((segment, index) => {\n\t\t\tconst prefix = index === 0 ? rowPrefix : continuationPrefix;\n\t\t\tconst line = `${prefix}${segment}`;\n\t\t\treturn applySelectedStyle ? this.theme.selectedText(line) : line;\n\t\t});\n\t}\n\n\tprivate renderDescriptionBlock(\n\t\tdescription: string | undefined,\n\t\tcontinuationPrefix: string,\n\t\tcontentWidth: number,\n\t): string[] {\n\t\tif (!description) return [];\n\t\tconst wrapped = wrapTextWithAnsi(description, contentWidth);\n\t\treturn wrapped.map((segment) => `${continuationPrefix}${this.theme.description(segment)}`);\n\t}\n}\n"]}
1
+ {"version":3,"file":"wrapping-select.js","sourceRoot":"","sources":["../../../../../../src/core/tools/ask-user-question/view/components/wrapping-select.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Component } from \"@earendil-works/pi-tui\";\nimport { visibleWidth, wrapTextWithAnsi } from \"@earendil-works/pi-tui\";\nimport { ROW_INTENT_META } from \"../../state/row-intent.ts\";\n\n/**\n * Row-intent discriminated union. `kind` is the single discriminator —\n * pre-1.0.3 boolean flags have been removed (see `banned-flags.test.ts`).\n * Modeled after `QuestionnaireAction` (`key-router.ts:13-32`) and `Effect`\n * (`state-reducer.ts:26-32`) — pure literal-tagged variants, no shared base,\n * exhaustive-`switch` enforcement via non-`void` returns.\n *\n * Variant semantics:\n * - `option`: a regular author-defined option row.\n * - `other`: the inline free-text input row appended to single-select questions\n * (label is \"Type something.\"). Renders as inline `Input` when active.\n * - `chat`: the abandon-questionnaire escape-hatch row (label is \"Chat about this\").\n * Renders as inline `Input` when active.\n * - `next`: the explicit commit-and-advance row appended to multi-select questions\n * (label is \"Next\"). Renders without a number / checkbox.\n */\nexport type WrappingSelectItem =\n\t| { kind: \"option\"; label: string; description?: string }\n\t| { kind: \"other\"; label: string; description?: string }\n\t| { kind: \"chat\"; label: string; description?: string }\n\t| { kind: \"next\"; label: string; description?: string };\n\nexport interface WrappingSelectTheme {\n\tselectedText: (text: string) => string;\n\tdescription: (text: string) => string;\n\tscrollInfo: (text: string) => string;\n}\n\n/**\n * Numbering controls.\n *\n * Use `numberStartOffset` + `totalItemsForNumbering` when a list is logically a slice of a\n * larger numbered sequence — e.g. the chat row lives in its own WrappingSelect but should\n * render as `(N+1).` where N is the previous list's item count, with the column padded as\n * if both lists were one continuous numbered sequence.\n */\nexport interface WrappingSelectOptions {\n\t/** Start numbering at this offset + 1 (default 0 → rows labeled 1, 2, 3 …). */\n\tnumberStartOffset?: number;\n\t/** Override the total used to pad the number column (useful when items span multiple lists). */\n\ttotalItemsForNumbering?: number;\n}\n\nexport class WrappingSelect implements Component {\n\tprivate static readonly ACTIVE_POINTER = \"❯ \";\n\tprivate static readonly INACTIVE_POINTER = \" \";\n\tprivate static readonly NUMBER_SEPARATOR = \". \";\n\tprivate static readonly CURSOR_INVERSE_START = \"\\x1b[7m\";\n\tprivate static readonly CURSOR_INVERSE_END = \"\\x1b[0m\";\n\tprivate static readonly CONFIRMED_MARK = \" ✔\";\n\tprivate static readonly MIN_CONTENT_WIDTH = 1;\n\n\tprivate readonly items: readonly WrappingSelectItem[];\n\tprivate readonly maxVisible: number;\n\tprivate readonly theme: WrappingSelectTheme;\n\tprivate numberStartOffset: number;\n\tprivate totalItemsForNumbering: number;\n\n\tprivate selectedIndex = 0;\n\tprivate focused = true;\n\tprivate inputBuffer = \"\";\n\tprivate inputCursor: number | undefined = undefined;\n\t/**\n\t * Index of the row that was previously confirmed for this list (e.g. the user's prior\n\t * answer when re-entering a multi-question tab). Renders `<label> ✔` in the active-row\n\t * styling but WITHOUT the `❯` pointer — pointer is reserved for the live cursor. When\n\t * `selectedIndex === confirmedIndex && focused`, the active rendering wins (no double-mark).\n\t */\n\tprivate confirmedIndex: number | undefined = undefined;\n\t/**\n\t * When set together with `confirmedIndex`, replaces the row's static label at render time.\n\t * Used for the `kind: \"other\"` sentinel — its label is \"Type something.\" but if the user's\n\t * prior answer was custom text, we render that text instead (e.g. `4. Hello ✔`).\n\t */\n\tprivate confirmedLabelOverride: string | undefined = undefined;\n\n\tconstructor(\n\t\titems: readonly WrappingSelectItem[],\n\t\tmaxVisible: number,\n\t\ttheme: WrappingSelectTheme,\n\t\toptions: WrappingSelectOptions = {},\n\t) {\n\t\tthis.items = items;\n\t\tthis.maxVisible = Math.max(1, maxVisible);\n\t\tthis.theme = theme;\n\t\tthis.numberStartOffset = options.numberStartOffset ?? 0;\n\t\tthis.totalItemsForNumbering = options.totalItemsForNumbering ?? items.length;\n\t}\n\n\t/**\n\t * Update the numbering offset + total padding width without rebuilding the component.\n\t * Used by the host to keep the chat-row WrappingSelect's number aligned with the active tab's\n\t * options list when the user switches tabs (each tab can have a different items count).\n\t */\n\tsetNumbering(numberStartOffset: number, totalItemsForNumbering: number): void {\n\t\tthis.numberStartOffset = numberStartOffset;\n\t\tthis.totalItemsForNumbering = Math.max(1, totalItemsForNumbering);\n\t}\n\n\tsetSelectedIndex(index: number): void {\n\t\tthis.selectedIndex = Math.max(0, Math.min(index, this.items.length - 1));\n\t}\n\n\tsetFocused(focused: boolean): void {\n\t\tthis.focused = focused;\n\t}\n\n\t/**\n\t * Mark a previously-confirmed row. Pass `undefined` to clear. `labelOverride` replaces\n\t * the row's static `item.label` at render time — used for the `kind: \"other\"` sentinel so\n\t * the row reads `Hello ✔` instead of `Type something. ✔` when the prior answer was custom\n\t * text.\n\t */\n\tsetConfirmedIndex(index: number | undefined, labelOverride?: string): void {\n\t\tif (index === undefined) {\n\t\t\tthis.confirmedIndex = undefined;\n\t\t\tthis.confirmedLabelOverride = undefined;\n\t\t\treturn;\n\t\t}\n\t\tthis.confirmedIndex = Math.max(0, Math.min(index, this.items.length - 1));\n\t\tthis.confirmedLabelOverride = labelOverride;\n\t}\n\n\tsetInputBuffer(text: string): void {\n\t\tthis.inputBuffer = text;\n\t\tif (this.inputCursor !== undefined) this.inputCursor = this.clampInputCursor(this.inputCursor);\n\t}\n\n\tsetInputCursor(cursor: number): void {\n\t\tthis.inputCursor = this.clampInputCursor(cursor);\n\t}\n\n\t/** Intentionally empty — input is routed at the container level. */\n\thandleInput(_data: string): void {}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tif (this.items.length === 0) return [];\n\n\t\tconst { startIndex, endIndex } = this.computeVisibleWindow();\n\t\tconst numberWidth = String(Math.max(1, this.totalItemsForNumbering)).length;\n\t\tconst lines: string[] = [];\n\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.items[i];\n\t\t\tif (!item) continue;\n\t\t\tconst isActive = i === this.selectedIndex && this.focused;\n\t\t\tlines.push(...this.renderItem(item, i, isActive, width, numberWidth));\n\t\t}\n\n\t\tif (this.hasItemsOutsideWindow(startIndex, endIndex)) {\n\t\t\tlines.push(this.theme.scrollInfo(` (${this.selectedIndex + 1}/${this.items.length})`));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate computeVisibleWindow(): { startIndex: number; endIndex: number } {\n\t\tconst half = Math.floor(this.maxVisible / 2);\n\t\tconst startIndex = Math.max(0, Math.min(this.selectedIndex - half, this.items.length - this.maxVisible));\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.items.length);\n\t\treturn { startIndex, endIndex };\n\t}\n\n\tprivate hasItemsOutsideWindow(startIndex: number, endIndex: number): boolean {\n\t\treturn startIndex > 0 || endIndex < this.items.length;\n\t}\n\n\tprivate renderItem(\n\t\titem: WrappingSelectItem,\n\t\tindex: number,\n\t\tisActive: boolean,\n\t\twidth: number,\n\t\tnumberWidth: number,\n\t): string[] {\n\t\tconst rowPrefix = this.buildRowPrefix(index, isActive, numberWidth);\n\t\tconst continuationPrefix = \" \".repeat(visibleWidth(rowPrefix));\n\t\tconst contentWidth = Math.max(WrappingSelect.MIN_CONTENT_WIDTH, width - visibleWidth(rowPrefix));\n\n\t\tif (this.shouldRenderAsInlineInput(item, isActive)) {\n\t\t\treturn this.renderInlineInputRow(rowPrefix, continuationPrefix, contentWidth);\n\t\t}\n\n\t\t// Confirmed row gets a trailing ` ✔` and accent+bold styling; pointer is independent\n\t\t// (still ❯ when active). When `index === confirmedIndex` AND `isActive`, both `❯` and\n\t\t// `✔` appear on the same row — load-bearing for the case where the prior answer was\n\t\t// row 0 (cursor resets to 0 on tab-back, so the confirmed row IS the active row).\n\t\t// Optional `confirmedLabelOverride` replaces the static label (used for `kind: \"other\"`\n\t\t// + `kind: \"custom\"` answer); the inline-input branch above still wins for `kind: \"other\" + isActive`.\n\t\tconst isConfirmed = index === this.confirmedIndex;\n\t\tconst label = isConfirmed\n\t\t\t? `${this.confirmedLabelOverride ?? item.label}${WrappingSelect.CONFIRMED_MARK}`\n\t\t\t: item.label;\n\t\tconst applySelectedStyle = isActive || isConfirmed;\n\n\t\treturn [\n\t\t\t...this.renderLabelBlock(label, rowPrefix, continuationPrefix, contentWidth, applySelectedStyle),\n\t\t\t...this.renderDescriptionBlock(item.description, continuationPrefix, contentWidth),\n\t\t];\n\t}\n\n\tprivate buildRowPrefix(index: number, isActive: boolean, numberWidth: number): string {\n\t\tconst pointer = isActive ? WrappingSelect.ACTIVE_POINTER : WrappingSelect.INACTIVE_POINTER;\n\t\tconst displayNumber = this.numberStartOffset + index + 1;\n\t\tconst paddedNumber = String(displayNumber).padStart(numberWidth, \" \");\n\t\treturn `${pointer}${paddedNumber}${WrappingSelect.NUMBER_SEPARATOR}`;\n\t}\n\n\tprivate shouldRenderAsInlineInput(item: WrappingSelectItem, isActive: boolean): boolean {\n\t\treturn isActive && ROW_INTENT_META[item.kind].activatesInputMode;\n\t}\n\n\t/**\n\t * Render the inline input row across one or more lines, wrapping at `contentWidth`\n\t * so long input doesn't run off the right edge or trip the parent renderer's\n\t * width invariant. Mirrors `renderLabelBlock`'s contract: first line carries\n\t * `rowPrefix`, continuation lines carry `continuationPrefix` (spaces), and every\n\t * emitted line passes through `theme.selectedText`. The cursor mirrors the\n\t * main chat editor: inverse-video over the character at the caret, or an\n\t * inverse-video space at end-of-line.\n\t */\n\tprivate renderInlineInputRow(rowPrefix: string, continuationPrefix: string, contentWidth: number): string[] {\n\t\tconst raw = this.inputTextWithCursor();\n\t\tconst wrapped = wrapTextWithAnsi(raw, contentWidth);\n\t\treturn wrapped.map((segment, index) => {\n\t\t\tconst prefix = index === 0 ? rowPrefix : continuationPrefix;\n\t\t\treturn this.theme.selectedText(`${prefix}${segment}`);\n\t\t});\n\t}\n\n\tprivate inputTextWithCursor(): string {\n\t\tconst cursor = this.clampInputCursor(this.inputCursor ?? this.inputBuffer.length);\n\t\tconst before = this.inputBuffer.slice(0, cursor);\n\t\tconst after = this.inputBuffer.slice(cursor);\n\t\tconst [at = \"\"] = Array.from(after);\n\t\tif (at === \"\") {\n\t\t\treturn `${before}${WrappingSelect.CURSOR_INVERSE_START} ${WrappingSelect.CURSOR_INVERSE_END}`;\n\t\t}\n\t\tconst rest = after.slice(at.length);\n\t\treturn `${before}${WrappingSelect.CURSOR_INVERSE_START}${at}${WrappingSelect.CURSOR_INVERSE_END}${rest}`;\n\t}\n\n\tprivate clampInputCursor(cursor: number): number {\n\t\tif (!Number.isFinite(cursor)) return this.inputBuffer.length;\n\t\treturn Math.max(0, Math.min(cursor, this.inputBuffer.length));\n\t}\n\n\tprivate renderLabelBlock(\n\t\tlabel: string,\n\t\trowPrefix: string,\n\t\tcontinuationPrefix: string,\n\t\tcontentWidth: number,\n\t\tapplySelectedStyle: boolean,\n\t): string[] {\n\t\tconst wrapped = wrapTextWithAnsi(label, contentWidth);\n\t\treturn wrapped.map((segment, index) => {\n\t\t\tconst prefix = index === 0 ? rowPrefix : continuationPrefix;\n\t\t\tconst line = `${prefix}${segment}`;\n\t\t\treturn applySelectedStyle ? this.theme.selectedText(line) : line;\n\t\t});\n\t}\n\n\tprivate renderDescriptionBlock(\n\t\tdescription: string | undefined,\n\t\tcontinuationPrefix: string,\n\t\tcontentWidth: number,\n\t): string[] {\n\t\tif (!description) return [];\n\t\tconst wrapped = wrapTextWithAnsi(description, contentWidth);\n\t\treturn wrapped.map((segment) => `${continuationPrefix}${this.theme.description(segment)}`);\n\t}\n}\n"]}
@@ -30,9 +30,9 @@ export interface QuestionnairePropsAdapterConfig {
30
30
  * two binding registries. `globalBindings` covers the cross-tab components
31
31
  * (chatRow, dialog, submitPicker?, tabBar?); `perTabBindings` covers the
32
32
  * per-tab kinds (optionList, preview, multiSelect?). The hand-coded fan-out
33
- * collapses to one global loop + one nested per-tab loop. The inline-Other
34
- * value is read from the headless `inlineInput` instance per tick into ctx so
35
- * `selectOptionListProps` sees the live value.
33
+ * collapses to one global loop + one nested per-tab loop. The shared inline
34
+ * input is projected through owner-specific draft slots so the option list and
35
+ * chat footer never display each other's in-flight text.
36
36
  */
37
37
  export declare class QuestionnairePropsAdapter {
38
38
  private readonly tui;
@@ -1 +1 @@
1
- {"version":3,"file":"props-adapter.d.ts","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/view/props-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAIpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,kGAAkG;AAClG,UAAU,aAAa;IACtB,UAAU,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,+BAA+B;IAC/C,GAAG,EAAE;QAAE,aAAa,IAAI,IAAI,CAAA;KAAE,CAAC;IAC/B,SAAS,EAAE,SAAS,YAAY,EAAE,CAAC;IACnC,UAAU,EAAE,aAAa,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;IACzD,WAAW,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAC1C,WAAW,EAAE,KAAK,CAAC;IACnB,cAAc,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;IAClD,cAAc,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;IAClD;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;CACnD;AAED;;;;;;;;GAQG;AACH,qBAAa,yBAAyB;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyC;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+C;IAC1E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAoC;IACnE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAoC;IACnE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA+B;IAEnE,YAAY,MAAM,EAAE,+BAA+B,EASlD;IAED,KAAK,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CA+BrC;IAED;;;;;;OAMG;IACH,UAAU,IAAI,IAAI,CAQjB;CACD","sourcesContent":["import type { Input } from \"@earendil-works/pi-tui\";\nimport type { BindingContext, PerTabBindingContext } from \"../state/selectors/contract.ts\";\nimport { selectActivePreviewPaneIndex } from \"../state/selectors/derivations.ts\";\nimport { selectActiveView } from \"../state/selectors/focus.ts\";\nimport type { QuestionnaireState } from \"../state/state.ts\";\nimport type { QuestionData } from \"../tool/types.ts\";\nimport type { BoundGlobalBinding, BoundPerTabBinding } from \"./component-binding.ts\";\nimport type { WrappingSelectItem } from \"./components/wrapping-select.ts\";\nimport type { TabComponents } from \"./tab-components.ts\";\n\n/** Cache-invalidation contract used by the adapter. `pi-tui` `Component` already satisfies it. */\ninterface Invalidatable {\n\tinvalidate(): void;\n}\n\nexport interface QuestionnairePropsAdapterConfig {\n\ttui: { requestRender(): void };\n\tquestions: readonly QuestionData[];\n\titemsByTab: ReadonlyArray<readonly WrappingSelectItem[]>;\n\ttabsByIndex: ReadonlyArray<TabComponents>;\n\tinlineInput: Input;\n\tglobalBindings: ReadonlyArray<BoundGlobalBinding>;\n\tperTabBindings: ReadonlyArray<BoundPerTabBinding>;\n\t/**\n\t * Renderables not reached by the binding registries (e.g. the notes\n\t * `Input`, which is typed into directly and has no props). Walked by\n\t * `invalidate()` after the binding-driven components.\n\t */\n\textraInvalidatables?: ReadonlyArray<Invalidatable>;\n}\n\n/**\n * View fan-out: drives every component setter from the canonical state via\n * two binding registries. `globalBindings` covers the cross-tab components\n * (chatRow, dialog, submitPicker?, tabBar?); `perTabBindings` covers the\n * per-tab kinds (optionList, preview, multiSelect?). The hand-coded fan-out\n * collapses to one global loop + one nested per-tab loop. The inline-Other\n * value is read from the headless `inlineInput` instance per tick into ctx so\n * `selectOptionListProps` sees the live value.\n */\nexport class QuestionnairePropsAdapter {\n\tprivate readonly tui: QuestionnairePropsAdapterConfig[\"tui\"];\n\tprivate readonly questions: readonly QuestionData[];\n\tprivate readonly itemsByTab: ReadonlyArray<readonly WrappingSelectItem[]>;\n\tprivate readonly tabsByIndex: ReadonlyArray<TabComponents>;\n\tprivate readonly inlineInput: Input;\n\tprivate readonly globalBindings: ReadonlyArray<BoundGlobalBinding>;\n\tprivate readonly perTabBindings: ReadonlyArray<BoundPerTabBinding>;\n\tprivate readonly extraInvalidatables: ReadonlyArray<Invalidatable>;\n\n\tconstructor(config: QuestionnairePropsAdapterConfig) {\n\t\tthis.tui = config.tui;\n\t\tthis.questions = config.questions;\n\t\tthis.itemsByTab = config.itemsByTab;\n\t\tthis.tabsByIndex = config.tabsByIndex;\n\t\tthis.inlineInput = config.inlineInput;\n\t\tthis.globalBindings = config.globalBindings;\n\t\tthis.perTabBindings = config.perTabBindings;\n\t\tthis.extraInvalidatables = config.extraInvalidatables ?? [];\n\t}\n\n\tapply(state: QuestionnaireState): void {\n\t\tconst totalQuestions = this.questions.length;\n\t\tconst activeView = selectActiveView(state, totalQuestions);\n\t\tconst paneIndex = selectActivePreviewPaneIndex(state.currentTab, totalQuestions);\n\t\tconst activePreviewPane = this.tabsByIndex[paneIndex]?.preview ?? this.tabsByIndex[0]!.preview;\n\n\t\tconst inputBuffer = state.customDraftByTab.get(state.currentTab) ?? this.inlineInput.getValue();\n\t\tconst inputCaret = state.customCaretByTab.get(state.currentTab) ?? inputBuffer.length;\n\t\tconst ctx: BindingContext = {\n\t\t\tquestions: this.questions,\n\t\t\titemsByTab: this.itemsByTab,\n\t\t\ttotalQuestions,\n\t\t\tactiveView,\n\t\t\tinputBuffer,\n\t\t\tinputCaret,\n\t\t\tactivePreviewPane,\n\t\t};\n\n\t\tfor (const binding of this.globalBindings) {\n\t\t\tbinding.apply(state, ctx);\n\t\t}\n\n\t\tfor (let i = 0; i < this.tabsByIndex.length; i++) {\n\t\t\tconst tab = this.tabsByIndex[i]!;\n\t\t\tconst tabCtx: PerTabBindingContext = { ...ctx, tab, i };\n\t\t\tfor (const binding of this.perTabBindings) {\n\t\t\t\tbinding.apply(state, tabCtx);\n\t\t\t}\n\t\t}\n\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Invalidates every owned renderable. Called by the session in place of\n\t * the old `dialog.invalidate()` forwarding chain — DialogView no longer\n\t * reaches into siblings (chatRow, tabBar, notesInput, activePreviewPane).\n\t * Iterates the same registries used by `apply()` plus\n\t * `extraInvalidatables` for components outside the binding system.\n\t */\n\tinvalidate(): void {\n\t\tfor (const b of this.globalBindings) b.invalidate();\n\t\tfor (const tab of this.tabsByIndex) {\n\t\t\ttab.optionList.invalidate();\n\t\t\ttab.preview.invalidate();\n\t\t\ttab.multiSelect?.invalidate();\n\t\t}\n\t\tfor (const x of this.extraInvalidatables) x.invalidate();\n\t}\n}\n"]}
1
+ {"version":3,"file":"props-adapter.d.ts","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/view/props-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAIpD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACrF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,kGAAkG;AAClG,UAAU,aAAa;IACtB,UAAU,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,+BAA+B;IAC/C,GAAG,EAAE;QAAE,aAAa,IAAI,IAAI,CAAA;KAAE,CAAC;IAC/B,SAAS,EAAE,SAAS,YAAY,EAAE,CAAC;IACnC,UAAU,EAAE,aAAa,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;IACzD,WAAW,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAC1C,WAAW,EAAE,KAAK,CAAC;IACnB,cAAc,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;IAClD,cAAc,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;IAClD;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;CACnD;AAED;;;;;;;;GAQG;AACH,qBAAa,yBAAyB;IACrC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAyC;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+C;IAC1E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAoC;IACnE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAoC;IACnE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA+B;IAEnE,YAAY,MAAM,EAAE,+BAA+B,EASlD;IAED,KAAK,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAwCrC;IAED;;;;;;OAMG;IACH,UAAU,IAAI,IAAI,CAQjB;CACD","sourcesContent":["import type { Input } from \"@earendil-works/pi-tui\";\nimport type { BindingContext, PerTabBindingContext } from \"../state/selectors/contract.ts\";\nimport { selectActivePreviewPaneIndex } from \"../state/selectors/derivations.ts\";\nimport { selectActiveView } from \"../state/selectors/focus.ts\";\nimport type { QuestionnaireState } from \"../state/state.ts\";\nimport type { QuestionData } from \"../tool/types.ts\";\nimport type { BoundGlobalBinding, BoundPerTabBinding } from \"./component-binding.ts\";\nimport type { WrappingSelectItem } from \"./components/wrapping-select.ts\";\nimport type { TabComponents } from \"./tab-components.ts\";\n\n/** Cache-invalidation contract used by the adapter. `pi-tui` `Component` already satisfies it. */\ninterface Invalidatable {\n\tinvalidate(): void;\n}\n\nexport interface QuestionnairePropsAdapterConfig {\n\ttui: { requestRender(): void };\n\tquestions: readonly QuestionData[];\n\titemsByTab: ReadonlyArray<readonly WrappingSelectItem[]>;\n\ttabsByIndex: ReadonlyArray<TabComponents>;\n\tinlineInput: Input;\n\tglobalBindings: ReadonlyArray<BoundGlobalBinding>;\n\tperTabBindings: ReadonlyArray<BoundPerTabBinding>;\n\t/**\n\t * Renderables not reached by the binding registries (e.g. the notes\n\t * `Input`, which is typed into directly and has no props). Walked by\n\t * `invalidate()` after the binding-driven components.\n\t */\n\textraInvalidatables?: ReadonlyArray<Invalidatable>;\n}\n\n/**\n * View fan-out: drives every component setter from the canonical state via\n * two binding registries. `globalBindings` covers the cross-tab components\n * (chatRow, dialog, submitPicker?, tabBar?); `perTabBindings` covers the\n * per-tab kinds (optionList, preview, multiSelect?). The hand-coded fan-out\n * collapses to one global loop + one nested per-tab loop. The shared inline\n * input is projected through owner-specific draft slots so the option list and\n * chat footer never display each other's in-flight text.\n */\nexport class QuestionnairePropsAdapter {\n\tprivate readonly tui: QuestionnairePropsAdapterConfig[\"tui\"];\n\tprivate readonly questions: readonly QuestionData[];\n\tprivate readonly itemsByTab: ReadonlyArray<readonly WrappingSelectItem[]>;\n\tprivate readonly tabsByIndex: ReadonlyArray<TabComponents>;\n\tprivate readonly inlineInput: Input;\n\tprivate readonly globalBindings: ReadonlyArray<BoundGlobalBinding>;\n\tprivate readonly perTabBindings: ReadonlyArray<BoundPerTabBinding>;\n\tprivate readonly extraInvalidatables: ReadonlyArray<Invalidatable>;\n\n\tconstructor(config: QuestionnairePropsAdapterConfig) {\n\t\tthis.tui = config.tui;\n\t\tthis.questions = config.questions;\n\t\tthis.itemsByTab = config.itemsByTab;\n\t\tthis.tabsByIndex = config.tabsByIndex;\n\t\tthis.inlineInput = config.inlineInput;\n\t\tthis.globalBindings = config.globalBindings;\n\t\tthis.perTabBindings = config.perTabBindings;\n\t\tthis.extraInvalidatables = config.extraInvalidatables ?? [];\n\t}\n\n\tapply(state: QuestionnaireState): void {\n\t\tconst totalQuestions = this.questions.length;\n\t\tconst activeView = selectActiveView(state, totalQuestions);\n\t\tconst paneIndex = selectActivePreviewPaneIndex(state.currentTab, totalQuestions);\n\t\tconst activePreviewPane = this.tabsByIndex[paneIndex]?.preview ?? this.tabsByIndex[0]!.preview;\n\n\t\tconst liveInlineValue = this.inlineInput.getValue();\n\t\tconst inputBuffer =\n\t\t\tstate.customDraftByTab.get(state.currentTab) ??\n\t\t\t(state.inlineInputOwner === \"other\" ? liveInlineValue : \"\");\n\t\tconst inputCaret = state.customCaretByTab.get(state.currentTab) ?? inputBuffer.length;\n\t\tconst chatInputBuffer =\n\t\t\tstate.chatDraftByTab.get(state.currentTab) ??\n\t\t\t(state.inlineInputOwner === \"chat\" ? liveInlineValue : \"\");\n\t\tconst chatInputCaret = state.chatCaretByTab.get(state.currentTab) ?? chatInputBuffer.length;\n\t\tconst ctx: BindingContext = {\n\t\t\tquestions: this.questions,\n\t\t\titemsByTab: this.itemsByTab,\n\t\t\ttotalQuestions,\n\t\t\tactiveView,\n\t\t\tinputBuffer,\n\t\t\tinputCaret,\n\t\t\tchatInputBuffer,\n\t\t\tchatInputCaret,\n\t\t\tactivePreviewPane,\n\t\t};\n\n\t\tfor (const binding of this.globalBindings) {\n\t\t\tbinding.apply(state, ctx);\n\t\t}\n\n\t\tfor (let i = 0; i < this.tabsByIndex.length; i++) {\n\t\t\tconst tab = this.tabsByIndex[i]!;\n\t\t\tconst tabCtx: PerTabBindingContext = { ...ctx, tab, i };\n\t\t\tfor (const binding of this.perTabBindings) {\n\t\t\t\tbinding.apply(state, tabCtx);\n\t\t\t}\n\t\t}\n\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Invalidates every owned renderable. Called by the session in place of\n\t * the old `dialog.invalidate()` forwarding chain — DialogView no longer\n\t * reaches into siblings (chatRow, tabBar, notesInput, activePreviewPane).\n\t * Iterates the same registries used by `apply()` plus\n\t * `extraInvalidatables` for components outside the binding system.\n\t */\n\tinvalidate(): void {\n\t\tfor (const b of this.globalBindings) b.invalidate();\n\t\tfor (const tab of this.tabsByIndex) {\n\t\t\ttab.optionList.invalidate();\n\t\t\ttab.preview.invalidate();\n\t\t\ttab.multiSelect?.invalidate();\n\t\t}\n\t\tfor (const x of this.extraInvalidatables) x.invalidate();\n\t}\n}\n"]}
@@ -5,9 +5,9 @@ import { selectActiveView } from "../state/selectors/focus.js";
5
5
  * two binding registries. `globalBindings` covers the cross-tab components
6
6
  * (chatRow, dialog, submitPicker?, tabBar?); `perTabBindings` covers the
7
7
  * per-tab kinds (optionList, preview, multiSelect?). The hand-coded fan-out
8
- * collapses to one global loop + one nested per-tab loop. The inline-Other
9
- * value is read from the headless `inlineInput` instance per tick into ctx so
10
- * `selectOptionListProps` sees the live value.
8
+ * collapses to one global loop + one nested per-tab loop. The shared inline
9
+ * input is projected through owner-specific draft slots so the option list and
10
+ * chat footer never display each other's in-flight text.
11
11
  */
12
12
  export class QuestionnairePropsAdapter {
13
13
  constructor(config) {
@@ -25,8 +25,13 @@ export class QuestionnairePropsAdapter {
25
25
  const activeView = selectActiveView(state, totalQuestions);
26
26
  const paneIndex = selectActivePreviewPaneIndex(state.currentTab, totalQuestions);
27
27
  const activePreviewPane = this.tabsByIndex[paneIndex]?.preview ?? this.tabsByIndex[0].preview;
28
- const inputBuffer = state.customDraftByTab.get(state.currentTab) ?? this.inlineInput.getValue();
28
+ const liveInlineValue = this.inlineInput.getValue();
29
+ const inputBuffer = state.customDraftByTab.get(state.currentTab) ??
30
+ (state.inlineInputOwner === "other" ? liveInlineValue : "");
29
31
  const inputCaret = state.customCaretByTab.get(state.currentTab) ?? inputBuffer.length;
32
+ const chatInputBuffer = state.chatDraftByTab.get(state.currentTab) ??
33
+ (state.inlineInputOwner === "chat" ? liveInlineValue : "");
34
+ const chatInputCaret = state.chatCaretByTab.get(state.currentTab) ?? chatInputBuffer.length;
30
35
  const ctx = {
31
36
  questions: this.questions,
32
37
  itemsByTab: this.itemsByTab,
@@ -34,6 +39,8 @@ export class QuestionnairePropsAdapter {
34
39
  activeView,
35
40
  inputBuffer,
36
41
  inputCaret,
42
+ chatInputBuffer,
43
+ chatInputCaret,
37
44
  activePreviewPane,
38
45
  };
39
46
  for (const binding of this.globalBindings) {
@@ -1 +1 @@
1
- {"version":3,"file":"props-adapter.js","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/view/props-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Input } from \"@earendil-works/pi-tui\";\nimport type { BindingContext, PerTabBindingContext } from \"../state/selectors/contract.ts\";\nimport { selectActivePreviewPaneIndex } from \"../state/selectors/derivations.ts\";\nimport { selectActiveView } from \"../state/selectors/focus.ts\";\nimport type { QuestionnaireState } from \"../state/state.ts\";\nimport type { QuestionData } from \"../tool/types.ts\";\nimport type { BoundGlobalBinding, BoundPerTabBinding } from \"./component-binding.ts\";\nimport type { WrappingSelectItem } from \"./components/wrapping-select.ts\";\nimport type { TabComponents } from \"./tab-components.ts\";\n\n/** Cache-invalidation contract used by the adapter. `pi-tui` `Component` already satisfies it. */\ninterface Invalidatable {\n\tinvalidate(): void;\n}\n\nexport interface QuestionnairePropsAdapterConfig {\n\ttui: { requestRender(): void };\n\tquestions: readonly QuestionData[];\n\titemsByTab: ReadonlyArray<readonly WrappingSelectItem[]>;\n\ttabsByIndex: ReadonlyArray<TabComponents>;\n\tinlineInput: Input;\n\tglobalBindings: ReadonlyArray<BoundGlobalBinding>;\n\tperTabBindings: ReadonlyArray<BoundPerTabBinding>;\n\t/**\n\t * Renderables not reached by the binding registries (e.g. the notes\n\t * `Input`, which is typed into directly and has no props). Walked by\n\t * `invalidate()` after the binding-driven components.\n\t */\n\textraInvalidatables?: ReadonlyArray<Invalidatable>;\n}\n\n/**\n * View fan-out: drives every component setter from the canonical state via\n * two binding registries. `globalBindings` covers the cross-tab components\n * (chatRow, dialog, submitPicker?, tabBar?); `perTabBindings` covers the\n * per-tab kinds (optionList, preview, multiSelect?). The hand-coded fan-out\n * collapses to one global loop + one nested per-tab loop. The inline-Other\n * value is read from the headless `inlineInput` instance per tick into ctx so\n * `selectOptionListProps` sees the live value.\n */\nexport class QuestionnairePropsAdapter {\n\tprivate readonly tui: QuestionnairePropsAdapterConfig[\"tui\"];\n\tprivate readonly questions: readonly QuestionData[];\n\tprivate readonly itemsByTab: ReadonlyArray<readonly WrappingSelectItem[]>;\n\tprivate readonly tabsByIndex: ReadonlyArray<TabComponents>;\n\tprivate readonly inlineInput: Input;\n\tprivate readonly globalBindings: ReadonlyArray<BoundGlobalBinding>;\n\tprivate readonly perTabBindings: ReadonlyArray<BoundPerTabBinding>;\n\tprivate readonly extraInvalidatables: ReadonlyArray<Invalidatable>;\n\n\tconstructor(config: QuestionnairePropsAdapterConfig) {\n\t\tthis.tui = config.tui;\n\t\tthis.questions = config.questions;\n\t\tthis.itemsByTab = config.itemsByTab;\n\t\tthis.tabsByIndex = config.tabsByIndex;\n\t\tthis.inlineInput = config.inlineInput;\n\t\tthis.globalBindings = config.globalBindings;\n\t\tthis.perTabBindings = config.perTabBindings;\n\t\tthis.extraInvalidatables = config.extraInvalidatables ?? [];\n\t}\n\n\tapply(state: QuestionnaireState): void {\n\t\tconst totalQuestions = this.questions.length;\n\t\tconst activeView = selectActiveView(state, totalQuestions);\n\t\tconst paneIndex = selectActivePreviewPaneIndex(state.currentTab, totalQuestions);\n\t\tconst activePreviewPane = this.tabsByIndex[paneIndex]?.preview ?? this.tabsByIndex[0]!.preview;\n\n\t\tconst inputBuffer = state.customDraftByTab.get(state.currentTab) ?? this.inlineInput.getValue();\n\t\tconst inputCaret = state.customCaretByTab.get(state.currentTab) ?? inputBuffer.length;\n\t\tconst ctx: BindingContext = {\n\t\t\tquestions: this.questions,\n\t\t\titemsByTab: this.itemsByTab,\n\t\t\ttotalQuestions,\n\t\t\tactiveView,\n\t\t\tinputBuffer,\n\t\t\tinputCaret,\n\t\t\tactivePreviewPane,\n\t\t};\n\n\t\tfor (const binding of this.globalBindings) {\n\t\t\tbinding.apply(state, ctx);\n\t\t}\n\n\t\tfor (let i = 0; i < this.tabsByIndex.length; i++) {\n\t\t\tconst tab = this.tabsByIndex[i]!;\n\t\t\tconst tabCtx: PerTabBindingContext = { ...ctx, tab, i };\n\t\t\tfor (const binding of this.perTabBindings) {\n\t\t\t\tbinding.apply(state, tabCtx);\n\t\t\t}\n\t\t}\n\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Invalidates every owned renderable. Called by the session in place of\n\t * the old `dialog.invalidate()` forwarding chain — DialogView no longer\n\t * reaches into siblings (chatRow, tabBar, notesInput, activePreviewPane).\n\t * Iterates the same registries used by `apply()` plus\n\t * `extraInvalidatables` for components outside the binding system.\n\t */\n\tinvalidate(): void {\n\t\tfor (const b of this.globalBindings) b.invalidate();\n\t\tfor (const tab of this.tabsByIndex) {\n\t\t\ttab.optionList.invalidate();\n\t\t\ttab.preview.invalidate();\n\t\t\ttab.multiSelect?.invalidate();\n\t\t}\n\t\tfor (const x of this.extraInvalidatables) x.invalidate();\n\t}\n}\n"]}
1
+ {"version":3,"file":"props-adapter.js","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/view/props-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Input } from \"@earendil-works/pi-tui\";\nimport type { BindingContext, PerTabBindingContext } from \"../state/selectors/contract.ts\";\nimport { selectActivePreviewPaneIndex } from \"../state/selectors/derivations.ts\";\nimport { selectActiveView } from \"../state/selectors/focus.ts\";\nimport type { QuestionnaireState } from \"../state/state.ts\";\nimport type { QuestionData } from \"../tool/types.ts\";\nimport type { BoundGlobalBinding, BoundPerTabBinding } from \"./component-binding.ts\";\nimport type { WrappingSelectItem } from \"./components/wrapping-select.ts\";\nimport type { TabComponents } from \"./tab-components.ts\";\n\n/** Cache-invalidation contract used by the adapter. `pi-tui` `Component` already satisfies it. */\ninterface Invalidatable {\n\tinvalidate(): void;\n}\n\nexport interface QuestionnairePropsAdapterConfig {\n\ttui: { requestRender(): void };\n\tquestions: readonly QuestionData[];\n\titemsByTab: ReadonlyArray<readonly WrappingSelectItem[]>;\n\ttabsByIndex: ReadonlyArray<TabComponents>;\n\tinlineInput: Input;\n\tglobalBindings: ReadonlyArray<BoundGlobalBinding>;\n\tperTabBindings: ReadonlyArray<BoundPerTabBinding>;\n\t/**\n\t * Renderables not reached by the binding registries (e.g. the notes\n\t * `Input`, which is typed into directly and has no props). Walked by\n\t * `invalidate()` after the binding-driven components.\n\t */\n\textraInvalidatables?: ReadonlyArray<Invalidatable>;\n}\n\n/**\n * View fan-out: drives every component setter from the canonical state via\n * two binding registries. `globalBindings` covers the cross-tab components\n * (chatRow, dialog, submitPicker?, tabBar?); `perTabBindings` covers the\n * per-tab kinds (optionList, preview, multiSelect?). The hand-coded fan-out\n * collapses to one global loop + one nested per-tab loop. The shared inline\n * input is projected through owner-specific draft slots so the option list and\n * chat footer never display each other's in-flight text.\n */\nexport class QuestionnairePropsAdapter {\n\tprivate readonly tui: QuestionnairePropsAdapterConfig[\"tui\"];\n\tprivate readonly questions: readonly QuestionData[];\n\tprivate readonly itemsByTab: ReadonlyArray<readonly WrappingSelectItem[]>;\n\tprivate readonly tabsByIndex: ReadonlyArray<TabComponents>;\n\tprivate readonly inlineInput: Input;\n\tprivate readonly globalBindings: ReadonlyArray<BoundGlobalBinding>;\n\tprivate readonly perTabBindings: ReadonlyArray<BoundPerTabBinding>;\n\tprivate readonly extraInvalidatables: ReadonlyArray<Invalidatable>;\n\n\tconstructor(config: QuestionnairePropsAdapterConfig) {\n\t\tthis.tui = config.tui;\n\t\tthis.questions = config.questions;\n\t\tthis.itemsByTab = config.itemsByTab;\n\t\tthis.tabsByIndex = config.tabsByIndex;\n\t\tthis.inlineInput = config.inlineInput;\n\t\tthis.globalBindings = config.globalBindings;\n\t\tthis.perTabBindings = config.perTabBindings;\n\t\tthis.extraInvalidatables = config.extraInvalidatables ?? [];\n\t}\n\n\tapply(state: QuestionnaireState): void {\n\t\tconst totalQuestions = this.questions.length;\n\t\tconst activeView = selectActiveView(state, totalQuestions);\n\t\tconst paneIndex = selectActivePreviewPaneIndex(state.currentTab, totalQuestions);\n\t\tconst activePreviewPane = this.tabsByIndex[paneIndex]?.preview ?? this.tabsByIndex[0]!.preview;\n\n\t\tconst liveInlineValue = this.inlineInput.getValue();\n\t\tconst inputBuffer =\n\t\t\tstate.customDraftByTab.get(state.currentTab) ??\n\t\t\t(state.inlineInputOwner === \"other\" ? liveInlineValue : \"\");\n\t\tconst inputCaret = state.customCaretByTab.get(state.currentTab) ?? inputBuffer.length;\n\t\tconst chatInputBuffer =\n\t\t\tstate.chatDraftByTab.get(state.currentTab) ??\n\t\t\t(state.inlineInputOwner === \"chat\" ? liveInlineValue : \"\");\n\t\tconst chatInputCaret = state.chatCaretByTab.get(state.currentTab) ?? chatInputBuffer.length;\n\t\tconst ctx: BindingContext = {\n\t\t\tquestions: this.questions,\n\t\t\titemsByTab: this.itemsByTab,\n\t\t\ttotalQuestions,\n\t\t\tactiveView,\n\t\t\tinputBuffer,\n\t\t\tinputCaret,\n\t\t\tchatInputBuffer,\n\t\t\tchatInputCaret,\n\t\t\tactivePreviewPane,\n\t\t};\n\n\t\tfor (const binding of this.globalBindings) {\n\t\t\tbinding.apply(state, ctx);\n\t\t}\n\n\t\tfor (let i = 0; i < this.tabsByIndex.length; i++) {\n\t\t\tconst tab = this.tabsByIndex[i]!;\n\t\t\tconst tabCtx: PerTabBindingContext = { ...ctx, tab, i };\n\t\t\tfor (const binding of this.perTabBindings) {\n\t\t\t\tbinding.apply(state, tabCtx);\n\t\t\t}\n\t\t}\n\n\t\tthis.tui.requestRender();\n\t}\n\n\t/**\n\t * Invalidates every owned renderable. Called by the session in place of\n\t * the old `dialog.invalidate()` forwarding chain — DialogView no longer\n\t * reaches into siblings (chatRow, tabBar, notesInput, activePreviewPane).\n\t * Iterates the same registries used by `apply()` plus\n\t * `extraInvalidatables` for components outside the binding system.\n\t */\n\tinvalidate(): void {\n\t\tfor (const b of this.globalBindings) b.invalidate();\n\t\tfor (const tab of this.tabsByIndex) {\n\t\t\ttab.optionList.invalidate();\n\t\t\ttab.preview.invalidate();\n\t\t\ttab.multiSelect?.invalidate();\n\t\t}\n\t\tfor (const x of this.extraInvalidatables) x.invalidate();\n\t}\n}\n"]}
@@ -0,0 +1,62 @@
1
+ export type BashCommandRule = string | {
2
+ readonly prefix: string;
3
+ } | {
4
+ readonly glob: string;
5
+ } | {
6
+ readonly regex: string;
7
+ readonly flags?: string;
8
+ };
9
+ export type BashCommandPolicyDefault = "allow" | "deny";
10
+ export type BashCommandPolicyMatchMode = "whole" | "segments";
11
+ export interface BashCommandPolicy {
12
+ readonly default?: BashCommandPolicyDefault;
13
+ readonly allow?: readonly BashCommandRule[];
14
+ readonly deny?: readonly BashCommandRule[];
15
+ readonly match?: BashCommandPolicyMatchMode;
16
+ }
17
+ export type BashCommandSegmentSource = "top-level" | "command-substitution" | "process-substitution" | "backtick";
18
+ export interface BashCommandSegment {
19
+ readonly raw: string;
20
+ readonly target: string;
21
+ readonly head: string;
22
+ readonly start: number;
23
+ readonly end: number;
24
+ readonly source: BashCommandSegmentSource;
25
+ }
26
+ export interface BashCommandParseError {
27
+ readonly reason: string;
28
+ readonly offset: number;
29
+ readonly source: BashCommandSegmentSource;
30
+ }
31
+ export type BashCommandParseResult = {
32
+ readonly ok: true;
33
+ readonly segments: readonly BashCommandSegment[];
34
+ } | {
35
+ readonly ok: false;
36
+ readonly error: BashCommandParseError;
37
+ };
38
+ export type BashCommandPolicyDenialReason = "invalid-policy" | "unsupported-shell-syntax" | "matched-deny" | "default-deny";
39
+ export interface BashCommandPolicyRejection {
40
+ readonly reason: BashCommandPolicyDenialReason;
41
+ readonly message: string;
42
+ readonly target?: BashCommandSegment;
43
+ readonly matchedRule?: BashCommandRule;
44
+ readonly parseError?: BashCommandParseError;
45
+ }
46
+ export type BashCommandPolicyDecision = {
47
+ readonly allowed: true;
48
+ readonly mode: BashCommandPolicyMatchMode;
49
+ readonly targets: readonly BashCommandSegment[];
50
+ } | {
51
+ readonly allowed: false;
52
+ readonly mode: BashCommandPolicyMatchMode;
53
+ readonly targets: readonly BashCommandSegment[];
54
+ readonly rejection: BashCommandPolicyRejection;
55
+ };
56
+ export declare function validateBashCommandPolicy(policy: BashCommandPolicy): void;
57
+ export declare function parseBashCommandSegments(command: string): BashCommandParseResult;
58
+ export declare function evaluateBashCommandPolicy(command: string, policy: BashCommandPolicy | undefined): BashCommandPolicyDecision;
59
+ export declare function formatBashCommandPolicyRejection(decision: Extract<BashCommandPolicyDecision, {
60
+ readonly allowed: false;
61
+ }>, policyLabel?: string): string;
62
+ //# sourceMappingURL=bash-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bash-policy.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash-policy.ts"],"names":[],"mappings":"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","sourcesContent":["export type BashCommandRule =\n\t| string\n\t| { readonly prefix: string }\n\t| { readonly glob: string }\n\t| { readonly regex: string; readonly flags?: string };\n\nexport type BashCommandPolicyDefault = \"allow\" | \"deny\";\nexport type BashCommandPolicyMatchMode = \"whole\" | \"segments\";\n\nexport interface BashCommandPolicy {\n\treadonly default?: BashCommandPolicyDefault;\n\treadonly allow?: readonly BashCommandRule[];\n\treadonly deny?: readonly BashCommandRule[];\n\treadonly match?: BashCommandPolicyMatchMode;\n}\n\nexport type BashCommandSegmentSource = \"top-level\" | \"command-substitution\" | \"process-substitution\" | \"backtick\";\n\nexport interface BashCommandSegment {\n\treadonly raw: string;\n\treadonly target: string;\n\treadonly head: string;\n\treadonly start: number;\n\treadonly end: number;\n\treadonly source: BashCommandSegmentSource;\n}\n\nexport interface BashCommandParseError {\n\treadonly reason: string;\n\treadonly offset: number;\n\treadonly source: BashCommandSegmentSource;\n}\n\nexport type BashCommandParseResult =\n\t| { readonly ok: true; readonly segments: readonly BashCommandSegment[] }\n\t| { readonly ok: false; readonly error: BashCommandParseError };\n\nexport type BashCommandPolicyDenialReason =\n\t| \"invalid-policy\"\n\t| \"unsupported-shell-syntax\"\n\t| \"matched-deny\"\n\t| \"default-deny\";\n\nexport interface BashCommandPolicyRejection {\n\treadonly reason: BashCommandPolicyDenialReason;\n\treadonly message: string;\n\treadonly target?: BashCommandSegment;\n\treadonly matchedRule?: BashCommandRule;\n\treadonly parseError?: BashCommandParseError;\n}\n\nexport type BashCommandPolicyDecision =\n\t| {\n\t\t\treadonly allowed: true;\n\t\t\treadonly mode: BashCommandPolicyMatchMode;\n\t\t\treadonly targets: readonly BashCommandSegment[];\n\t }\n\t| {\n\t\t\treadonly allowed: false;\n\t\t\treadonly mode: BashCommandPolicyMatchMode;\n\t\t\treadonly targets: readonly BashCommandSegment[];\n\t\t\treadonly rejection: BashCommandPolicyRejection;\n\t };\n\ntype CompiledRule =\n\t| { readonly kind: \"exact\"; readonly source: BashCommandRule; readonly value: string }\n\t| { readonly kind: \"prefix\"; readonly source: BashCommandRule; readonly value: string }\n\t| { readonly kind: \"glob\"; readonly source: BashCommandRule; readonly value: RegExp }\n\t| { readonly kind: \"regex\"; readonly source: BashCommandRule; readonly value: RegExp };\n\ninterface CompiledBashCommandPolicy {\n\treadonly defaultDecision: BashCommandPolicyDefault;\n\treadonly match: BashCommandPolicyMatchMode;\n\treadonly allow: readonly CompiledRule[];\n\treadonly deny: readonly CompiledRule[];\n}\n\ntype CompileResult =\n\t| { readonly ok: true; readonly policy: CompiledBashCommandPolicy }\n\t| { readonly ok: false; readonly message: string };\n\ntype CompileRuleResult =\n\t| { readonly ok: true; readonly rule: CompiledRule }\n\t| { readonly ok: false; readonly message: string };\n\ntype CompileRulesResult =\n\t| { readonly ok: true; readonly rules: readonly CompiledRule[] }\n\t| { readonly ok: false; readonly message: string };\n\ntype SegmentBuildResult =\n\t| { readonly ok: true; readonly segment?: BashCommandSegment }\n\t| { readonly ok: false; readonly error: BashCommandParseError };\n\ntype ClosingParenResult =\n\t| { readonly ok: true; readonly closeIndex: number }\n\t| { readonly ok: false; readonly reason: string; readonly offset: number };\n\ntype ClosingBacktickResult =\n\t| { readonly ok: true; readonly closeIndex: number }\n\t| { readonly ok: false; readonly reason: string; readonly offset: number };\n\ntype ShellQuoteState = \"none\" | \"single\" | \"double\";\n\ninterface ShellWordMetadata {\n\treadonly sawSingleQuote: boolean;\n\treadonly sawDoubleQuote: boolean;\n\treadonly sawEscape: boolean;\n\treadonly sawParameterExpansion: boolean;\n\treadonly sawCommandSubstitution: boolean;\n\treadonly sawProcessSubstitution: boolean;\n\treadonly sawBacktick: boolean;\n\treadonly sawGlobPattern: boolean;\n\treadonly sawBraceExpansion: boolean;\n\treadonly sawTildePrefix: boolean;\n}\n\ntype ShellWordReadResult =\n\t| { readonly ok: true; readonly word: string; readonly end: number; readonly metadata: ShellWordMetadata }\n\t| { readonly ok: false; readonly reason: string; readonly offset: number };\n\ntype LiteralCommandHeadValidation =\n\t| { readonly ok: true }\n\t| { readonly ok: false; readonly reason: string };\n\nconst UNSUPPORTED_CONTROL_HEADS = new Set([\n\t\"!\",\n\t\"[[\",\n\t\"]]\",\n\t\"case\",\n\t\"coproc\",\n\t\"do\",\n\t\"done\",\n\t\"elif\",\n\t\"else\",\n\t\"esac\",\n\t\"fi\",\n\t\"for\",\n\t\"function\",\n\t\"if\",\n\t\"in\",\n\t\"select\",\n\t\"then\",\n\t\"time\",\n\t\"until\",\n\t\"while\",\n\t\"{\",\n\t\"}\",\n]);\n\nconst DIAGNOSTIC_TEXT_LIMIT = 220;\n\nfunction truncateDiagnostic(text: string): string {\n\tif (text.length <= DIAGNOSTIC_TEXT_LIMIT) return text;\n\treturn `${text.slice(0, DIAGNOSTIC_TEXT_LIMIT - 1)}…`;\n}\n\ntype RuntimePropertyValue =\n\t| string\n\t| number\n\t| boolean\n\t| bigint\n\t| symbol\n\t| null\n\t| object\n\t| ((...args: readonly never[]) => RuntimePropertyValue)\n\t| undefined;\n\ntype RuleObjectKey = \"prefix\" | \"glob\" | \"regex\" | \"flags\";\n\nconst BASH_POLICY_TOP_LEVEL_KEYS = [\"default\", \"allow\", \"deny\", \"match\"] as const;\nconst BASH_POLICY_TOP_LEVEL_KEY_SET: ReadonlySet<string> = new Set(BASH_POLICY_TOP_LEVEL_KEYS);\n\nfunction hasOwnRuleKey<Key extends RuleObjectKey>(\n\tvalue: object,\n\tkey: Key,\n): value is Record<Key, RuntimePropertyValue> {\n\treturn Object.prototype.hasOwnProperty.call(value, key);\n}\n\nfunction hasOnlyRuleKeys(value: object, allowedKeys: readonly string[]): boolean {\n\treturn Object.keys(value).every((key) => allowedKeys.includes(key));\n}\n\nfunction unknownBashPolicyTopLevelKeys(policy: object): readonly string[] {\n\treturn Object.keys(policy).filter((key) => !BASH_POLICY_TOP_LEVEL_KEY_SET.has(key));\n}\n\nfunction escapeRegexLiteral(value: string): string {\n\treturn value.replace(/[\\\\^$.*+?()[\\]{}|]/g, \"\\\\$&\");\n}\n\nfunction escapeGlobClassCharacter(char: string): string {\n\tif (char === \"\\\\\") return \"\\\\\\\\\";\n\tif (char === \"]\") return \"\\\\]\";\n\tif (char === \"[\") return \"\\\\[\";\n\treturn char;\n}\n\nfunction escapeEscapedGlobClassCharacter(char: string): string {\n\tif (char === \"\\\\\") return \"\\\\\\\\\";\n\tif (char === \"-\") return \"\\\\-\";\n\tif (char === \"^\") return \"\\\\^\";\n\tif (char === \"]\") return \"\\\\]\";\n\tif (char === \"[\") return \"\\\\[\";\n\treturn char;\n}\n\nfunction readGlobBracketClass(\n\tpattern: string,\n\topenIndex: number,\n): { readonly regexSource: string; readonly closeIndex: number } | undefined {\n\tlet cursor = openIndex + 1;\n\tlet negated = false;\n\tlet hasContent = false;\n\tlet content = \"\";\n\n\tif (cursor >= pattern.length) return undefined;\n\tif (pattern[cursor] === \"!\" || pattern[cursor] === \"^\") {\n\t\tnegated = true;\n\t\tcursor += 1;\n\t}\n\tif (pattern[cursor] === \"]\") {\n\t\tcontent += \"\\\\]\";\n\t\thasContent = true;\n\t\tcursor += 1;\n\t}\n\n\tfor (; cursor < pattern.length; cursor += 1) {\n\t\tconst char = pattern[cursor]!;\n\t\tif (char === \"]\") {\n\t\t\tif (!hasContent) return undefined;\n\t\t\treturn { regexSource: `[${negated ? \"^\" : \"\"}${content}]`, closeIndex: cursor };\n\t\t}\n\t\thasContent = true;\n\t\tif (char === \"\\\\\" && cursor + 1 < pattern.length) {\n\t\t\tcursor += 1;\n\t\t\tcontent += escapeEscapedGlobClassCharacter(pattern[cursor]!);\n\t\t\tcontinue;\n\t\t}\n\t\tcontent += escapeGlobClassCharacter(char);\n\t}\n\n\treturn undefined;\n}\n\nfunction compileCommandStringGlob(pattern: string): RegExp {\n\tlet source = \"^\";\n\tfor (let index = 0; index < pattern.length; index += 1) {\n\t\tconst char = pattern[index]!;\n\t\tif (char === \"*\") {\n\t\t\tsource += \".*\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"?\") {\n\t\t\tsource += \".\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"[\") {\n\t\t\tconst bracket = readGlobBracketClass(pattern, index);\n\t\t\tif (bracket !== undefined) {\n\t\t\t\tsource += bracket.regexSource;\n\t\t\t\tindex = bracket.closeIndex;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tif (char === \"\\\\\" && index + 1 < pattern.length) {\n\t\t\tindex += 1;\n\t\t\tsource += escapeRegexLiteral(pattern[index]!);\n\t\t\tcontinue;\n\t\t}\n\t\tsource += escapeRegexLiteral(char);\n\t}\n\treturn new RegExp(`${source}$`);\n}\n\nfunction compileRule(rule: BashCommandRule, listName: \"allow\" | \"deny\", index: number): CompileRuleResult {\n\tif (typeof rule === \"string\") {\n\t\tif (rule.length === 0) {\n\t\t\treturn { ok: false, message: `${listName}[${index}] exact rule must not be empty` };\n\t\t}\n\t\treturn { ok: true, rule: { kind: \"exact\", source: rule, value: rule } };\n\t}\n\n\tif (typeof rule !== \"object\" || rule === null || Array.isArray(rule)) {\n\t\treturn { ok: false, message: `${listName}[${index}] rule must be a non-empty string or an object rule` };\n\t}\n\n\tconst hasPrefix = hasOwnRuleKey(rule, \"prefix\");\n\tconst hasGlob = hasOwnRuleKey(rule, \"glob\");\n\tconst hasRegex = hasOwnRuleKey(rule, \"regex\");\n\tconst variantCount = (hasPrefix ? 1 : 0) + (hasGlob ? 1 : 0) + (hasRegex ? 1 : 0);\n\tif (variantCount !== 1) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `${listName}[${index}] must specify exactly one of prefix, glob, or regex`,\n\t\t};\n\t}\n\n\tif (hasPrefix) {\n\t\tif (!hasOnlyRuleKeys(rule, [\"prefix\"])) {\n\t\t\treturn { ok: false, message: `${listName}[${index}] prefix rule must only contain prefix` };\n\t\t}\n\t\tif (typeof rule.prefix !== \"string\") {\n\t\t\treturn { ok: false, message: `${listName}[${index}].prefix must be a string` };\n\t\t}\n\t\tif (rule.prefix.length === 0) {\n\t\t\treturn { ok: false, message: `${listName}[${index}].prefix must not be empty` };\n\t\t}\n\t\treturn { ok: true, rule: { kind: \"prefix\", source: rule, value: rule.prefix } };\n\t}\n\n\tif (hasGlob) {\n\t\tif (!hasOnlyRuleKeys(rule, [\"glob\"])) {\n\t\t\treturn { ok: false, message: `${listName}[${index}] glob rule must only contain glob` };\n\t\t}\n\t\tif (typeof rule.glob !== \"string\") {\n\t\t\treturn { ok: false, message: `${listName}[${index}].glob must be a string` };\n\t\t}\n\t\tif (rule.glob.length === 0) {\n\t\t\treturn { ok: false, message: `${listName}[${index}].glob must not be empty` };\n\t\t}\n\t\ttry {\n\t\t\treturn { ok: true, rule: { kind: \"glob\", source: rule, value: compileCommandStringGlob(rule.glob) } };\n\t\t} catch {\n\t\t\treturn { ok: false, message: `${listName}[${index}].glob is not a valid command string glob` };\n\t\t}\n\t}\n\n\tif (!hasOnlyRuleKeys(rule, [\"regex\", \"flags\"])) {\n\t\treturn { ok: false, message: `${listName}[${index}] regex rule must only contain regex and optional flags` };\n\t}\n\tif (typeof rule.regex !== \"string\") {\n\t\treturn { ok: false, message: `${listName}[${index}].regex must be a string` };\n\t}\n\tif (rule.regex.length === 0) {\n\t\treturn { ok: false, message: `${listName}[${index}].regex must not be empty` };\n\t}\n\tconst hasFlags = hasOwnRuleKey(rule, \"flags\");\n\tif (hasFlags && typeof rule.flags !== \"string\") {\n\t\treturn { ok: false, message: `${listName}[${index}].flags must be a string when present` };\n\t}\n\tconst flags = hasFlags && typeof rule.flags === \"string\" ? rule.flags : \"\";\n\tif (/[gy]/.test(flags)) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `${listName}[${index}].flags must not include stateful g or y regex flags`,\n\t\t};\n\t}\n\ttry {\n\t\treturn { ok: true, rule: { kind: \"regex\", source: rule, value: new RegExp(rule.regex, flags) } };\n\t} catch {\n\t\treturn { ok: false, message: `${listName}[${index}].regex is not a valid JavaScript RegExp` };\n\t}\n}\n\nfunction compileRules(rules: readonly BashCommandRule[] | undefined, listName: \"allow\" | \"deny\"): CompileRulesResult {\n\tconst compiled: CompiledRule[] = [];\n\tif (rules === undefined) return { ok: true, rules: compiled };\n\tfor (let index = 0; index < rules.length; index += 1) {\n\t\tconst rule = rules[index]!;\n\t\tconst result = compileRule(rule, listName, index);\n\t\tif (!result.ok) return result;\n\t\tcompiled.push(result.rule);\n\t}\n\treturn { ok: true, rules: compiled };\n}\n\nfunction compilePolicy(policy: BashCommandPolicy): CompileResult {\n\tif (typeof policy !== \"object\" || policy === null || Array.isArray(policy)) {\n\t\treturn { ok: false, message: \"bash policy must be a non-null object\" };\n\t}\n\n\tconst unknownKeys = unknownBashPolicyTopLevelKeys(policy);\n\tif (unknownKeys.length > 0) {\n\t\tconst formattedKeys = unknownKeys.map((key) => JSON.stringify(key)).join(\", \");\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `bash policy contains unknown top-level key${unknownKeys.length === 1 ? \"\" : \"s\"} ${formattedKeys}; allowed keys are default, allow, deny, and match`,\n\t\t};\n\t}\n\n\tconst defaultDecision = policy.default === undefined ? \"allow\" : policy.default;\n\tif (defaultDecision !== \"allow\" && defaultDecision !== \"deny\") {\n\t\treturn { ok: false, message: `bash policy default must be \"allow\" or \"deny\"` };\n\t}\n\tconst match = policy.match === undefined ? \"segments\" : policy.match;\n\tif (match !== \"whole\" && match !== \"segments\") {\n\t\treturn { ok: false, message: `bash policy match must be \"whole\" or \"segments\"` };\n\t}\n\tif (policy.allow !== undefined && !Array.isArray(policy.allow)) {\n\t\treturn { ok: false, message: \"bash policy allow must be an array\" };\n\t}\n\tif (policy.deny !== undefined && !Array.isArray(policy.deny)) {\n\t\treturn { ok: false, message: \"bash policy deny must be an array\" };\n\t}\n\n\tconst allow = compileRules(policy.allow, \"allow\");\n\tif (!allow.ok) return allow;\n\tconst deny = compileRules(policy.deny, \"deny\");\n\tif (!deny.ok) return deny;\n\n\treturn {\n\t\tok: true,\n\t\tpolicy: {\n\t\t\tdefaultDecision,\n\t\t\tmatch,\n\t\t\tallow: allow.rules,\n\t\t\tdeny: deny.rules,\n\t\t},\n\t};\n}\n\nexport function validateBashCommandPolicy(policy: BashCommandPolicy): void {\n\tconst compiled = compilePolicy(policy);\n\tif (!compiled.ok) {\n\t\tthrow new Error(`Invalid bash command policy: ${compiled.message}`);\n\t}\n}\n\nfunction shellError(reason: string, offset: number, source: BashCommandSegmentSource): BashCommandParseResult {\n\treturn { ok: false, error: { reason, offset, source } };\n}\n\nfunction isWhitespace(char: string): boolean {\n\treturn char === \" \" || char === \"\\t\" || char === \"\\n\" || char === \"\\r\";\n}\n\nfunction lineTerminatorLengthAt(input: string, index: number): number {\n\tconst char = input[index];\n\tif (char === \"\\r\") return input[index + 1] === \"\\n\" ? 2 : 1;\n\tif (char === \"\\n\") return 1;\n\treturn 0;\n}\n\nfunction previousNonWhitespace(input: string, index: number): string | undefined {\n\tfor (let i = index - 1; i >= 0; i -= 1) {\n\t\tconst char = input[i]!;\n\t\tif (!isWhitespace(char)) return char;\n\t}\n\treturn undefined;\n}\n\nfunction isRedirectionAmpersand(input: string, index: number): boolean {\n\tconst previous = previousNonWhitespace(input, index);\n\tconst next = input[index + 1];\n\treturn previous === \">\" || previous === \"<\" || next === \">\";\n}\n\nfunction operatorLengthAt(input: string, index: number): number {\n\tconst char = input[index];\n\tconst next = input[index + 1];\n\tif (char === \"|\" && input[index - 1] === \">\") return 0;\n\tif (char === \"|\" && next === \"&\") return 2;\n\tif (char === \"&\" && next === \"&\") return 2;\n\tif (char === \"|\" && next === \"|\") return 2;\n\tif (char === \"|\") return 1;\n\tif (char === \";\") return 1;\n\tif (char === \"&\" && !isRedirectionAmpersand(input, index)) return 1;\n\treturn 0;\n}\n\nfunction isHereDocumentAt(input: string, index: number): boolean {\n\treturn input[index] === \"<\" && input[index + 1] === \"<\";\n}\n\nfunction isCommandSubstitutionAt(input: string, index: number): boolean {\n\treturn input[index] === \"$\" && input[index + 1] === \"(\";\n}\n\nfunction isProcessSubstitutionAt(input: string, index: number): boolean {\n\tconst char = input[index];\n\treturn (char === \"<\" || char === \">\") && input[index + 1] === \"(\";\n}\n\nfunction findClosingBacktick(input: string, openIndex: number): ClosingBacktickResult {\n\tfor (let i = openIndex + 1; i < input.length; i += 1) {\n\t\tconst char = input[i]!;\n\t\tif (char === \"\\\\\") {\n\t\t\tif (i + 1 >= input.length) {\n\t\t\t\treturn { ok: false, reason: \"trailing escape in backtick command substitution\", offset: i };\n\t\t\t}\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"`\") return { ok: true, closeIndex: i };\n\t}\n\treturn { ok: false, reason: \"unclosed backtick command substitution\", offset: openIndex };\n}\n\nfunction findClosingParen(input: string, openIndex: number, construct: string): ClosingParenResult {\n\tlet quote: ShellQuoteState = \"none\";\n\tlet depth = 1;\n\tfor (let i = openIndex + 1; i < input.length; i += 1) {\n\t\tconst char = input[i]!;\n\n\t\tif (quote === \"single\") {\n\t\t\tif (char === \"'\") quote = \"none\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"\\\\\") {\n\t\t\tif (i + 1 >= input.length) {\n\t\t\t\treturn { ok: false, reason: `trailing escape in ${construct}`, offset: i };\n\t\t\t}\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (quote === \"double\") {\n\t\t\tif (char === \"\\\"\") {\n\t\t\t\tquote = \"none\";\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {\n\t\t\t\tdepth += 1;\n\t\t\t\ti += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (char === \"`\") {\n\t\t\t\tconst close = findClosingBacktick(input, i);\n\t\t\t\tif (!close.ok) return close;\n\t\t\t\ti = close.closeIndex;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"'\") {\n\t\t\tquote = \"single\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"\\\"\") {\n\t\t\tquote = \"double\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {\n\t\t\tdepth += 1;\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"`\") {\n\t\t\tconst close = findClosingBacktick(input, i);\n\t\t\tif (!close.ok) return close;\n\t\t\ti = close.closeIndex;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"(\") {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\treason: `unsupported shell grouping parentheses in ${construct}`,\n\t\t\t\toffset: i,\n\t\t\t};\n\t\t}\n\t\tif (char === \")\") {\n\t\t\tdepth -= 1;\n\t\t\tif (depth === 0) return { ok: true, closeIndex: i };\n\t\t}\n\t}\n\n\tif (quote === \"single\") return { ok: false, reason: `unclosed single quote in ${construct}`, offset: openIndex };\n\tif (quote === \"double\") return { ok: false, reason: `unclosed double quote in ${construct}`, offset: openIndex };\n\treturn { ok: false, reason: `unclosed ${construct}`, offset: openIndex };\n}\n\nfunction readShellWord(input: string, start: number): ShellWordReadResult {\n\tlet quote: ShellQuoteState = \"none\";\n\tlet sawSingleQuote = false;\n\tlet sawDoubleQuote = false;\n\tlet sawEscape = false;\n\tlet sawParameterExpansion = false;\n\tlet sawCommandSubstitution = false;\n\tlet sawProcessSubstitution = false;\n\tlet sawBacktick = false;\n\tlet sawGlobPattern = false;\n\tlet sawBraceExpansion = false;\n\tlet sawTildePrefix = false;\n\n\tconst metadata = (): ShellWordMetadata => ({\n\t\tsawSingleQuote,\n\t\tsawDoubleQuote,\n\t\tsawEscape,\n\t\tsawParameterExpansion,\n\t\tsawCommandSubstitution,\n\t\tsawProcessSubstitution,\n\t\tsawBacktick,\n\t\tsawGlobPattern,\n\t\tsawBraceExpansion,\n\t\tsawTildePrefix,\n\t});\n\n\tfor (let i = start; i < input.length; i += 1) {\n\t\tconst char = input[i]!;\n\t\tif (quote === \"single\") {\n\t\t\tif (char === \"'\") quote = \"none\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"\\\\\") {\n\t\t\tsawEscape = true;\n\t\t\tif (i + 1 >= input.length) return { ok: false, reason: \"trailing escape in shell word\", offset: i };\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote === \"double\") {\n\t\t\tif (char === \"\\\"\") {\n\t\t\t\tquote = \"none\";\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {\n\t\t\t\tif (isCommandSubstitutionAt(input, i)) {\n\t\t\t\t\tsawCommandSubstitution = true;\n\t\t\t\t} else {\n\t\t\t\t\tsawProcessSubstitution = true;\n\t\t\t\t}\n\t\t\t\tconst close = findClosingParen(input, i + 1, isCommandSubstitutionAt(input, i) ? \"command substitution `$(`\" : \"process substitution\");\n\t\t\t\tif (!close.ok) return { ok: false, reason: close.reason, offset: close.offset };\n\t\t\t\ti = close.closeIndex;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (char === \"`\") {\n\t\t\t\tsawBacktick = true;\n\t\t\t\tconst close = findClosingBacktick(input, i);\n\t\t\t\tif (!close.ok) return { ok: false, reason: close.reason, offset: close.offset };\n\t\t\t\ti = close.closeIndex;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (char === \"$\") sawParameterExpansion = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (isWhitespace(char)) {\n\t\t\treturn { ok: true, word: input.slice(start, i), end: i, metadata: metadata() };\n\t\t}\n\t\tif (char === \"'\") {\n\t\t\tsawSingleQuote = true;\n\t\t\tquote = \"single\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"\\\"\") {\n\t\t\tsawDoubleQuote = true;\n\t\t\tquote = \"double\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (i === start && char === \"~\") {\n\t\t\tsawTildePrefix = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {\n\t\t\tif (isCommandSubstitutionAt(input, i)) {\n\t\t\t\tsawCommandSubstitution = true;\n\t\t\t} else {\n\t\t\t\tsawProcessSubstitution = true;\n\t\t\t}\n\t\t\tconst close = findClosingParen(input, i + 1, isCommandSubstitutionAt(input, i) ? \"command substitution `$(`\" : \"process substitution\");\n\t\t\tif (!close.ok) return { ok: false, reason: close.reason, offset: close.offset };\n\t\t\ti = close.closeIndex;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"`\") {\n\t\t\tsawBacktick = true;\n\t\t\tconst close = findClosingBacktick(input, i);\n\t\t\tif (!close.ok) return { ok: false, reason: close.reason, offset: close.offset };\n\t\t\ti = close.closeIndex;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"$\") {\n\t\t\tsawParameterExpansion = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"*\" || char === \"?\" || char === \"[\" || char === \"]\") {\n\t\t\tsawGlobPattern = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"{\" || char === \"}\") {\n\t\t\tsawBraceExpansion = true;\n\t\t}\n\t}\n\tif (quote === \"single\") return { ok: false, reason: \"unclosed single quote in shell word\", offset: start };\n\tif (quote === \"double\") return { ok: false, reason: \"unclosed double quote in shell word\", offset: start };\n\treturn { ok: true, word: input.slice(start), end: input.length, metadata: metadata() };\n}\n\nfunction isAsciiDigit(char: string | undefined): boolean {\n\treturn char !== undefined && char >= \"0\" && char <= \"9\";\n}\n\nfunction leadingRedirectionTokenAt(input: string, index: number): string | undefined {\n\tlet operatorStart = index;\n\twhile (isAsciiDigit(input[operatorStart])) operatorStart += 1;\n\n\tconst hasDescriptorPrefix = operatorStart > index;\n\tconst char = input[operatorStart];\n\tconst next = input[operatorStart + 1];\n\tconst afterNext = input[operatorStart + 2];\n\n\tif (char === undefined) return undefined;\n\tif (hasDescriptorPrefix && char !== \"<\" && char !== \">\") return undefined;\n\n\tif (!hasDescriptorPrefix && (char === \"<\" || char === \">\") && next === \"(\") {\n\t\treturn undefined;\n\t}\n\n\tlet operator: string | undefined;\n\tif (char === \"&\" && next === \">\") {\n\t\toperator = afterNext === \">\" ? \"&>>\" : \"&>\";\n\t} else if (char === \"<\") {\n\t\tif (next === \"<\") operator = \"<<\";\n\t\telse if (next === \"&\") operator = \"<&\";\n\t\telse if (next === \">\") operator = \"<>\";\n\t\telse operator = \"<\";\n\t} else if (char === \">\") {\n\t\tif (next === \">\") operator = \">>\";\n\t\telse if (next === \"|\") operator = \">|\";\n\t\telse if (next === \"&\") operator = \">&\";\n\t\telse operator = \">\";\n\t}\n\n\treturn operator === undefined ? undefined : `${input.slice(index, operatorStart)}${operator}`;\n}\n\nfunction isEnvAssignmentWord(word: string): boolean {\n\treturn /^[A-Za-z_][A-Za-z0-9_]*(?:\\+)?=/.test(word);\n}\n\ninterface AttachedRedirectionToken {\n\treadonly token: string;\n\treadonly offset: number;\n}\n\nfunction attachedRedirectionOperatorAt(input: string, index: number): string | undefined {\n\tconst char = input[index];\n\tconst next = input[index + 1];\n\tconst afterNext = input[index + 2];\n\n\tif ((char === \"<\" || char === \">\") && next === \"(\") {\n\t\treturn undefined;\n\t}\n\tif (char === \"&\" && next === \">\") {\n\t\treturn afterNext === \">\" ? \"&>>\" : \"&>\";\n\t}\n\tif (char === \"<\") {\n\t\tif (next === \"<\") return \"<<\";\n\t\tif (next === \"&\") return \"<&\";\n\t\tif (next === \">\") return \"<>\";\n\t\treturn \"<\";\n\t}\n\tif (char === \">\") {\n\t\tif (next === \">\") return \">>\";\n\t\tif (next === \"|\") return \">|\";\n\t\tif (next === \"&\") return \">&\";\n\t\treturn \">\";\n\t}\n\treturn undefined;\n}\n\nfunction attachedCommandHeadRedirection(\n\tinput: string,\n\tstart: number,\n\tend: number,\n): AttachedRedirectionToken | undefined {\n\tlet quote: ShellQuoteState = \"none\";\n\n\tfor (let i = start; i < end; i += 1) {\n\t\tconst char = input[i]!;\n\t\tif (quote === \"single\") {\n\t\t\tif (char === \"'\") quote = \"none\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"\\\\\") {\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote === \"double\") {\n\t\t\tif (char === \"\\\"\") {\n\t\t\t\tquote = \"none\";\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {\n\t\t\t\tconst close = findClosingParen(input, i + 1, isCommandSubstitutionAt(input, i) ? \"command substitution `$(`\" : \"process substitution\");\n\t\t\t\tif (close.ok) i = close.closeIndex;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (char === \"`\") {\n\t\t\t\tconst close = findClosingBacktick(input, i);\n\t\t\t\tif (close.ok) i = close.closeIndex;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"'\") {\n\t\t\tquote = \"single\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"\\\"\") {\n\t\t\tquote = \"double\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (isCommandSubstitutionAt(input, i) || isProcessSubstitutionAt(input, i)) {\n\t\t\tconst close = findClosingParen(input, i + 1, isCommandSubstitutionAt(input, i) ? \"command substitution `$(`\" : \"process substitution\");\n\t\t\tif (close.ok) i = close.closeIndex;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"`\") {\n\t\t\tconst close = findClosingBacktick(input, i);\n\t\t\tif (close.ok) i = close.closeIndex;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst operator = attachedRedirectionOperatorAt(input, i);\n\t\tif (operator !== undefined && i > start) {\n\t\t\treturn { token: operator, offset: i };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\nfunction validateLiteralCommandHead(head: string, metadata: ShellWordMetadata): LiteralCommandHeadValidation {\n\tif (head.length === 0) {\n\t\treturn { ok: false, reason: \"empty command heads are not supported by bash policy segments mode\" };\n\t}\n\tif (metadata.sawSingleQuote || metadata.sawDoubleQuote) {\n\t\treturn { ok: false, reason: \"quoted or quote-constructed command heads are not supported by bash policy segments mode\" };\n\t}\n\tif (metadata.sawEscape) {\n\t\treturn { ok: false, reason: \"escape-constructed command heads are not supported by bash policy segments mode\" };\n\t}\n\tif (metadata.sawCommandSubstitution || metadata.sawProcessSubstitution || metadata.sawBacktick) {\n\t\treturn { ok: false, reason: \"command, process, and backtick substitutions are not supported in command heads by bash policy segments mode\" };\n\t}\n\tif (metadata.sawParameterExpansion) {\n\t\treturn { ok: false, reason: \"parameter-expanded command heads are not supported by bash policy segments mode\" };\n\t}\n\tif (metadata.sawTildePrefix) {\n\t\treturn { ok: false, reason: \"tilde-expanded command heads are not supported by bash policy segments mode\" };\n\t}\n\tif (metadata.sawGlobPattern) {\n\t\treturn { ok: false, reason: \"glob-expanded command heads are not supported by bash policy segments mode\" };\n\t}\n\tif (metadata.sawBraceExpansion) {\n\t\treturn { ok: false, reason: \"brace-expanded command heads are not supported by bash policy segments mode\" };\n\t}\n\treturn { ok: true };\n}\n\nfunction buildSegment(\n\trawSegment: string,\n\tabsoluteStart: number,\n\tabsoluteEnd: number,\n\tsource: BashCommandSegmentSource,\n): SegmentBuildResult {\n\tlet cursor = 0;\n\twhile (cursor < rawSegment.length && isWhitespace(rawSegment[cursor]!)) cursor += 1;\n\tif (cursor >= rawSegment.length) return { ok: true };\n\n\twhile (cursor < rawSegment.length) {\n\t\twhile (cursor < rawSegment.length && isWhitespace(rawSegment[cursor]!)) cursor += 1;\n\t\tif (cursor >= rawSegment.length) return { ok: true };\n\n\t\tconst leadingRedirection = leadingRedirectionTokenAt(rawSegment, cursor);\n\t\tif (leadingRedirection !== undefined) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: {\n\t\t\t\t\treason: `leading shell redirection ${JSON.stringify(leadingRedirection)} is not supported by bash policy segments mode`,\n\t\t\t\t\toffset: absoluteStart + cursor,\n\t\t\t\t\tsource,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst word = readShellWord(rawSegment, cursor);\n\t\tif (!word.ok) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: { reason: word.reason, offset: absoluteStart + word.offset, source },\n\t\t\t};\n\t\t}\n\n\t\tconst attachedRedirection = attachedCommandHeadRedirection(rawSegment, cursor, word.end);\n\t\tif (attachedRedirection !== undefined) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: {\n\t\t\t\t\treason: `attached shell redirection ${JSON.stringify(attachedRedirection.token)} in the command head is not supported by bash policy segments mode`,\n\t\t\t\t\toffset: absoluteStart + attachedRedirection.offset,\n\t\t\t\t\tsource,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (isEnvAssignmentWord(word.word)) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: {\n\t\t\t\t\treason: \"environment assignment words are not supported by bash policy segments mode\",\n\t\t\t\t\toffset: absoluteStart + cursor,\n\t\t\t\t\tsource,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst target = rawSegment.slice(cursor).trim();\n\t\tconst head = word.word;\n\t\tif (UNSUPPORTED_CONTROL_HEADS.has(head)) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: {\n\t\t\t\t\treason: `unsupported shell reserved or compound syntax starting with ${JSON.stringify(head)}`,\n\t\t\t\t\toffset: absoluteStart + cursor,\n\t\t\t\t\tsource,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconst literalHead = validateLiteralCommandHead(head, word.metadata);\n\t\tif (!literalHead.ok) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: {\n\t\t\t\t\treason: literalHead.reason,\n\t\t\t\t\toffset: absoluteStart + cursor,\n\t\t\t\t\tsource,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tok: true,\n\t\t\tsegment: {\n\t\t\t\traw: rawSegment.trim(),\n\t\t\t\ttarget,\n\t\t\t\thead,\n\t\t\t\tstart: absoluteStart + cursor,\n\t\t\t\tend: absoluteEnd,\n\t\t\t\tsource,\n\t\t\t},\n\t\t};\n\t}\n\n\treturn { ok: true };\n}\n\nfunction parseSegmentsInSource(\n\tinput: string,\n\tbaseOffset: number,\n\tsource: BashCommandSegmentSource,\n): BashCommandParseResult {\n\tconst segments: BashCommandSegment[] = [];\n\tlet quote: ShellQuoteState = \"none\";\n\tlet segmentStart = 0;\n\tlet nestedForCurrentSegment: BashCommandSegment[] = [];\n\n\tconst commitSegment = (end: number): BashCommandParseResult | undefined => {\n\t\tconst built = buildSegment(input.slice(segmentStart, end), baseOffset + segmentStart, baseOffset + end, source);\n\t\tif (!built.ok) return { ok: false, error: built.error };\n\t\tif (built.segment) segments.push(built.segment);\n\t\tsegments.push(...nestedForCurrentSegment);\n\t\tnestedForCurrentSegment = [];\n\t\treturn undefined;\n\t};\n\n\tfor (let i = 0; i < input.length; i += 1) {\n\t\tconst char = input[i]!;\n\n\t\tif (quote === \"single\") {\n\t\t\tif (char === \"'\") quote = \"none\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"\\\\\") {\n\t\t\tif (i + 1 >= input.length) {\n\t\t\t\treturn shellError(\"trailing escape\", baseOffset + i, source);\n\t\t\t}\n\t\t\ti += 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (quote === \"double\") {\n\t\t\tif (char === \"\\\"\") {\n\t\t\t\tquote = \"none\";\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (isCommandSubstitutionAt(input, i)) {\n\t\t\t\tconst close = findClosingParen(input, i + 1, \"command substitution `$(`\");\n\t\t\t\tif (!close.ok) return shellError(close.reason, baseOffset + close.offset, source);\n\t\t\t\tconst nested = parseSegmentsInSource(input.slice(i + 2, close.closeIndex), baseOffset + i + 2, \"command-substitution\");\n\t\t\t\tif (!nested.ok) return nested;\n\t\t\t\tnestedForCurrentSegment.push(...nested.segments);\n\t\t\t\ti = close.closeIndex;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (char === \"`\") {\n\t\t\t\tconst close = findClosingBacktick(input, i);\n\t\t\t\tif (!close.ok) return shellError(close.reason, baseOffset + close.offset, source);\n\t\t\t\tconst nested = parseSegmentsInSource(input.slice(i + 1, close.closeIndex), baseOffset + i + 1, \"backtick\");\n\t\t\t\tif (!nested.ok) return nested;\n\t\t\t\tnestedForCurrentSegment.push(...nested.segments);\n\t\t\t\ti = close.closeIndex;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \"'\") {\n\t\t\tquote = \"single\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"\\\"\") {\n\t\t\tquote = \"double\";\n\t\t\tcontinue;\n\t\t}\n\t\tif (isHereDocumentAt(input, i)) {\n\t\t\treturn shellError(\"here-documents are not supported by bash policy segments mode\", baseOffset + i, source);\n\t\t}\n\t\tif (isCommandSubstitutionAt(input, i)) {\n\t\t\tconst close = findClosingParen(input, i + 1, \"command substitution `$(`\");\n\t\t\tif (!close.ok) return shellError(close.reason, baseOffset + close.offset, source);\n\t\t\tconst nested = parseSegmentsInSource(input.slice(i + 2, close.closeIndex), baseOffset + i + 2, \"command-substitution\");\n\t\t\tif (!nested.ok) return nested;\n\t\t\tnestedForCurrentSegment.push(...nested.segments);\n\t\t\ti = close.closeIndex;\n\t\t\tcontinue;\n\t\t}\n\t\tif (isProcessSubstitutionAt(input, i)) {\n\t\t\tconst close = findClosingParen(input, i + 1, \"process substitution\");\n\t\t\tif (!close.ok) return shellError(close.reason, baseOffset + close.offset, source);\n\t\t\tconst nested = parseSegmentsInSource(input.slice(i + 2, close.closeIndex), baseOffset + i + 2, \"process-substitution\");\n\t\t\tif (!nested.ok) return nested;\n\t\t\tnestedForCurrentSegment.push(...nested.segments);\n\t\t\ti = close.closeIndex;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"`\") {\n\t\t\tconst close = findClosingBacktick(input, i);\n\t\t\tif (!close.ok) return shellError(close.reason, baseOffset + close.offset, source);\n\t\t\tconst nested = parseSegmentsInSource(input.slice(i + 1, close.closeIndex), baseOffset + i + 1, \"backtick\");\n\t\t\tif (!nested.ok) return nested;\n\t\t\tnestedForCurrentSegment.push(...nested.segments);\n\t\t\ti = close.closeIndex;\n\t\t\tcontinue;\n\t\t}\n\t\tif (char === \"(\" || char === \")\") {\n\t\t\treturn shellError(\"unsupported shell grouping parentheses\", baseOffset + i, source);\n\t\t}\n\n\t\tconst lineTerminatorLength = lineTerminatorLengthAt(input, i);\n\t\tif (lineTerminatorLength > 0) {\n\t\t\tconst committed = commitSegment(i);\n\t\t\tif (committed) return committed;\n\t\t\ti += lineTerminatorLength - 1;\n\t\t\tsegmentStart = i + 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst operatorLength = operatorLengthAt(input, i);\n\t\tif (operatorLength > 0) {\n\t\t\tconst committed = commitSegment(i);\n\t\t\tif (committed) return committed;\n\t\t\ti += operatorLength - 1;\n\t\t\tsegmentStart = i + 1;\n\t\t}\n\t}\n\n\tif (quote === \"single\") return shellError(\"unclosed single quote\", baseOffset + input.length, source);\n\tif (quote === \"double\") return shellError(\"unclosed double quote\", baseOffset + input.length, source);\n\tconst committed = commitSegment(input.length);\n\tif (committed) return committed;\n\treturn { ok: true, segments };\n}\n\nexport function parseBashCommandSegments(command: string): BashCommandParseResult {\n\treturn parseSegmentsInSource(command, 0, \"top-level\");\n}\n\nfunction wholeCommandTarget(command: string): BashCommandSegment {\n\tconst target = command;\n\tconst trimmed = command.trimStart();\n\tconst leading = command.length - trimmed.length;\n\tconst firstSpace = trimmed.search(/\\s/);\n\tconst head = firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);\n\treturn {\n\t\traw: command,\n\t\ttarget,\n\t\thead,\n\t\tstart: leading,\n\t\tend: command.length,\n\t\tsource: \"top-level\",\n\t};\n}\n\nfunction ruleMatches(rule: CompiledRule, target: string): boolean {\n\tswitch (rule.kind) {\n\t\tcase \"exact\":\n\t\t\treturn target === rule.value;\n\t\tcase \"prefix\":\n\t\t\treturn target.startsWith(rule.value);\n\t\tcase \"glob\":\n\t\t\treturn rule.value.test(target);\n\t\tcase \"regex\":\n\t\t\treturn rule.value.test(target);\n\t}\n}\n\nfunction firstMatchingRule(rules: readonly CompiledRule[], target: string): BashCommandRule | undefined {\n\tfor (const rule of rules) {\n\t\tif (ruleMatches(rule, target)) return rule.source;\n\t}\n\treturn undefined;\n}\n\nfunction invalidPolicyMode(policy: BashCommandPolicy): BashCommandPolicyMatchMode {\n\tif (typeof policy !== \"object\" || policy === null || Array.isArray(policy)) return \"segments\";\n\treturn policy.match === \"whole\" ? \"whole\" : \"segments\";\n}\n\nfunction isNoRuleDefaultAllowPolicy(policy: CompiledBashCommandPolicy): boolean {\n\treturn policy.defaultDecision === \"allow\" && policy.allow.length === 0 && policy.deny.length === 0;\n}\n\nexport function evaluateBashCommandPolicy(\n\tcommand: string,\n\tpolicy: BashCommandPolicy | undefined,\n): BashCommandPolicyDecision {\n\tif (policy === undefined) {\n\t\treturn { allowed: true, mode: \"segments\", targets: [] };\n\t}\n\n\tconst compiled = compilePolicy(policy);\n\tif (!compiled.ok) {\n\t\treturn {\n\t\t\tallowed: false,\n\t\t\tmode: invalidPolicyMode(policy),\n\t\t\ttargets: [],\n\t\t\trejection: {\n\t\t\t\treason: \"invalid-policy\",\n\t\t\t\tmessage: compiled.message,\n\t\t\t},\n\t\t};\n\t}\n\n\tconst activePolicy = compiled.policy;\n\tif (isNoRuleDefaultAllowPolicy(activePolicy)) {\n\t\treturn { allowed: true, mode: activePolicy.match, targets: [] };\n\t}\n\n\tconst targetsResult: BashCommandParseResult = activePolicy.match === \"whole\"\n\t\t? { ok: true, segments: [wholeCommandTarget(command)] }\n\t\t: parseBashCommandSegments(command);\n\n\tif (!targetsResult.ok) {\n\t\treturn {\n\t\t\tallowed: false,\n\t\t\tmode: activePolicy.match,\n\t\t\ttargets: [],\n\t\t\trejection: {\n\t\t\t\treason: \"unsupported-shell-syntax\",\n\t\t\t\tmessage: targetsResult.error.reason,\n\t\t\t\tparseError: targetsResult.error,\n\t\t\t},\n\t\t};\n\t}\n\n\tfor (const target of targetsResult.segments) {\n\t\tconst denyRule = firstMatchingRule(activePolicy.deny, target.target);\n\t\tif (denyRule !== undefined) {\n\t\t\treturn {\n\t\t\t\tallowed: false,\n\t\t\t\tmode: activePolicy.match,\n\t\t\t\ttargets: targetsResult.segments,\n\t\t\t\trejection: {\n\t\t\t\t\treason: \"matched-deny\",\n\t\t\t\t\tmessage: `command ${JSON.stringify(target.head)} matched a deny rule`,\n\t\t\t\t\ttarget,\n\t\t\t\t\tmatchedRule: denyRule,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst allowRule = firstMatchingRule(activePolicy.allow, target.target);\n\t\tif (allowRule !== undefined || activePolicy.defaultDecision === \"allow\") {\n\t\t\tcontinue;\n\t\t}\n\n\t\treturn {\n\t\t\tallowed: false,\n\t\t\tmode: activePolicy.match,\n\t\t\ttargets: targetsResult.segments,\n\t\t\trejection: {\n\t\t\t\treason: \"default-deny\",\n\t\t\t\tmessage: `command ${JSON.stringify(target.head)} is not permitted by default-deny bash policy`,\n\t\t\t\ttarget,\n\t\t\t},\n\t\t};\n\t}\n\n\treturn { allowed: true, mode: activePolicy.match, targets: targetsResult.segments };\n}\n\nfunction formatRule(rule: BashCommandRule): string {\n\tif (typeof rule === \"string\") return JSON.stringify(rule);\n\tif (\"prefix\" in rule) return `{ prefix: ${JSON.stringify(rule.prefix)} }`;\n\tif (\"glob\" in rule) return `{ glob: ${JSON.stringify(rule.glob)} }`;\n\treturn rule.flags === undefined\n\t\t? `{ regex: ${JSON.stringify(rule.regex)} }`\n\t\t: `{ regex: ${JSON.stringify(rule.regex)}, flags: ${JSON.stringify(rule.flags)} }`;\n}\n\nexport function formatBashCommandPolicyRejection(\n\tdecision: Extract<BashCommandPolicyDecision, { readonly allowed: false }>,\n\tpolicyLabel = \"bash command policy\",\n): string {\n\tconst lines = [`Bash command blocked by ${policyLabel}.`, \"\"];\n\tconst rejection = decision.rejection;\n\n\tif (rejection.reason === \"unsupported-shell-syntax\") {\n\t\tlines.push(\n\t\t\t\"The command uses shell syntax that Atomic cannot safely parse in `segments` mode.\",\n\t\t\t`Reason: ${rejection.message}.`,\n\t\t);\n\t\tif (rejection.parseError) {\n\t\t\tlines.push(`Parser source: ${rejection.parseError.source} at offset ${rejection.parseError.offset}.`);\n\t\t}\n\t\tlines.push(\n\t\t\t\"Use match: \\\"whole\\\" only if the caller intentionally accepts raw-command matching semantics.\",\n\t\t);\n\t} else if (rejection.reason === \"invalid-policy\") {\n\t\tlines.push(\"The configured bash command policy is invalid.\", `Reason: ${rejection.message}.`);\n\t} else {\n\t\tconst target = rejection.target;\n\t\tif (target) {\n\t\t\tlines.push(\n\t\t\t\t`Command head: \\`${truncateDiagnostic(target.head)}\\``,\n\t\t\t\t`Rejected ${decision.mode === \"whole\" ? \"command\" : \"segment\"}: \\`${truncateDiagnostic(target.target)}\\``,\n\t\t\t\t`Segment source: ${target.source}`,\n\t\t\t);\n\t\t}\n\t\tif (rejection.reason === \"matched-deny\") {\n\t\t\tlines.push(\"Reason: matched a deny rule; deny rules take precedence over allow rules.\");\n\t\t\tif (rejection.matchedRule !== undefined) {\n\t\t\t\tlines.push(`Matched deny rule: ${formatRule(rejection.matchedRule)}`);\n\t\t\t}\n\t\t} else {\n\t\t\tlines.push(\"Reason: no allow rule matched and the policy default is deny.\");\n\t\t}\n\t}\n\n\tlines.push(`Policy mode: ${decision.mode}.`, \"\", \"No shell process was started.\");\n\treturn lines.join(\"\\n\");\n}\n"]}