dataface 0.1.2__py3-none-any.whl

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 (455) hide show
  1. d3_format/__init__.py +14 -0
  2. d3_format/errors.py +19 -0
  3. d3_format/format.py +551 -0
  4. d3_format/spec.py +159 -0
  5. dataface/DATAFACE_SYNTAX.md +1135 -0
  6. dataface/__init__.py +93 -0
  7. dataface/_docs_site.py +20 -0
  8. dataface/_install_hint.py +26 -0
  9. dataface/agent_api/__init__.py +79 -0
  10. dataface/agent_api/_init_templates/__init__.py +0 -0
  11. dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
  12. dataface/agent_api/_init_templates/dataface.yml +15 -0
  13. dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
  14. dataface/agent_api/_init_templates/index.md +24 -0
  15. dataface/agent_api/_paths.py +118 -0
  16. dataface/agent_api/_project_agents_md.py +43 -0
  17. dataface/agent_api/_session_store.py +486 -0
  18. dataface/agent_api/_state.py +28 -0
  19. dataface/agent_api/chat.py +221 -0
  20. dataface/agent_api/dashboards.py +257 -0
  21. dataface/agent_api/describe.py +366 -0
  22. dataface/agent_api/describe_query.py +120 -0
  23. dataface/agent_api/docs/__init__.py +25 -0
  24. dataface/agent_api/docs/_loader.py +292 -0
  25. dataface/agent_api/docs/yaml-reference.md +2757 -0
  26. dataface/agent_api/file_refs.py +118 -0
  27. dataface/agent_api/init.py +126 -0
  28. dataface/agent_api/inspect.py +128 -0
  29. dataface/agent_api/mcp_install.py +170 -0
  30. dataface/agent_api/query.py +274 -0
  31. dataface/agent_api/schema.py +658 -0
  32. dataface/agent_api/schema_search.py +284 -0
  33. dataface/agent_api/search.py +270 -0
  34. dataface/agent_api/skill_install.py +141 -0
  35. dataface/agent_api/skill_render.py +90 -0
  36. dataface/agent_api/skills.py +293 -0
  37. dataface/agent_api/surface_aliases.yaml +128 -0
  38. dataface/agent_api/validate.py +175 -0
  39. dataface/agent_api/validate_query.py +84 -0
  40. dataface/ai/__init__.py +39 -0
  41. dataface/ai/agent.py +139 -0
  42. dataface/ai/context.py +45 -0
  43. dataface/ai/events.py +62 -0
  44. dataface/ai/external_mcp.py +610 -0
  45. dataface/ai/generate_sql.py +96 -0
  46. dataface/ai/llm.py +403 -0
  47. dataface/ai/mcp/__init__.py +51 -0
  48. dataface/ai/mcp/server.py +289 -0
  49. dataface/ai/memories.py +85 -0
  50. dataface/ai/prompts.py +177 -0
  51. dataface/ai/schema_context.py +138 -0
  52. dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
  53. dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
  54. dataface/ai/skills/dashboard-build/SKILL.md +212 -0
  55. dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
  56. dataface/ai/skills/dashboard-design/SKILL.md +182 -0
  57. dataface/ai/skills/dashboard-review/SKILL.md +113 -0
  58. dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
  59. dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
  60. dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
  61. dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
  62. dataface/ai/skills/drill-down-link/SKILL.md +112 -0
  63. dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
  64. dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
  65. dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
  66. dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
  67. dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
  68. dataface/ai/skills/kpi-row/SKILL.md +101 -0
  69. dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
  70. dataface/ai/skills/report-design/SKILL.md +184 -0
  71. dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
  72. dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
  73. dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
  74. dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
  75. dataface/ai/skills/time-series-trend/SKILL.md +93 -0
  76. dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
  77. dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
  78. dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
  79. dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
  80. dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
  81. dataface/ai/tool_schemas.py +132 -0
  82. dataface/ai/tools/__init__.py +312 -0
  83. dataface/ai/yaml_utils.py +57 -0
  84. dataface/cli/__init__.py +3 -0
  85. dataface/cli/_console.py +48 -0
  86. dataface/cli/_error_format.py +83 -0
  87. dataface/cli/_extras.py +190 -0
  88. dataface/cli/_json_output.py +8 -0
  89. dataface/cli/_parsing.py +17 -0
  90. dataface/cli/_version_info.py +56 -0
  91. dataface/cli/commands/__init__.py +3 -0
  92. dataface/cli/commands/_agent_input.py +205 -0
  93. dataface/cli/commands/_agent_server.py +115 -0
  94. dataface/cli/commands/chat.py +645 -0
  95. dataface/cli/commands/describe.py +107 -0
  96. dataface/cli/commands/docs.py +131 -0
  97. dataface/cli/commands/extension.py +179 -0
  98. dataface/cli/commands/init.py +240 -0
  99. dataface/cli/commands/inspect.py +94 -0
  100. dataface/cli/commands/mcp_init.py +167 -0
  101. dataface/cli/commands/query.py +386 -0
  102. dataface/cli/commands/render.py +291 -0
  103. dataface/cli/commands/schema.py +411 -0
  104. dataface/cli/commands/search.py +49 -0
  105. dataface/cli/commands/serve.py +114 -0
  106. dataface/cli/commands/skills.py +133 -0
  107. dataface/cli/commands/skills_init.py +161 -0
  108. dataface/cli/commands/validate.py +63 -0
  109. dataface/cli/main.py +1501 -0
  110. dataface/core/__init__.py +75 -0
  111. dataface/core/compile/__init__.py +244 -0
  112. dataface/core/compile/_jinja_helpers.py +78 -0
  113. dataface/core/compile/channel.py +222 -0
  114. dataface/core/compile/chart_focus.py +101 -0
  115. dataface/core/compile/chart_resolved.py +169 -0
  116. dataface/core/compile/chart_type_detection.py +489 -0
  117. dataface/core/compile/chart_update.py +261 -0
  118. dataface/core/compile/colors.py +64 -0
  119. dataface/core/compile/compiler.py +904 -0
  120. dataface/core/compile/config.py +823 -0
  121. dataface/core/compile/custom_chart_types.py +208 -0
  122. dataface/core/compile/data_table_attachment.py +1287 -0
  123. dataface/core/compile/detect.py +110 -0
  124. dataface/core/compile/errors.py +302 -0
  125. dataface/core/compile/filter_injection.py +319 -0
  126. dataface/core/compile/introspection.py +527 -0
  127. dataface/core/compile/jinja.py +511 -0
  128. dataface/core/compile/labels_env.py +52 -0
  129. dataface/core/compile/markdown.py +154 -0
  130. dataface/core/compile/meta.py +388 -0
  131. dataface/core/compile/models/__init__.py +0 -0
  132. dataface/core/compile/models/chart/__init__.py +0 -0
  133. dataface/core/compile/models/chart/authored.py +2137 -0
  134. dataface/core/compile/models/chart/compiled.py +398 -0
  135. dataface/core/compile/models/config.py +347 -0
  136. dataface/core/compile/models/face/__init__.py +0 -0
  137. dataface/core/compile/models/face/authored.py +659 -0
  138. dataface/core/compile/models/face/compiled.py +522 -0
  139. dataface/core/compile/models/factories.py +201 -0
  140. dataface/core/compile/models/markers.py +40 -0
  141. dataface/core/compile/models/palette.py +36 -0
  142. dataface/core/compile/models/primitives.py +415 -0
  143. dataface/core/compile/models/query/__init__.py +0 -0
  144. dataface/core/compile/models/query/authored.py +246 -0
  145. dataface/core/compile/models/query/compiled.py +710 -0
  146. dataface/core/compile/models/refs.py +137 -0
  147. dataface/core/compile/models/source.py +611 -0
  148. dataface/core/compile/models/style/__init__.py +0 -0
  149. dataface/core/compile/models/style/authored.py +481 -0
  150. dataface/core/compile/models/style/compiled.py +3399 -0
  151. dataface/core/compile/models/style/merged.py +1682 -0
  152. dataface/core/compile/models/theme.py +362 -0
  153. dataface/core/compile/models/variable/__init__.py +0 -0
  154. dataface/core/compile/models/variable/authored.py +254 -0
  155. dataface/core/compile/models/vega_lite/__init__.py +0 -0
  156. dataface/core/compile/models/vega_lite/config.py +510 -0
  157. dataface/core/compile/models/vega_lite/contracts.py +171 -0
  158. dataface/core/compile/normalize_charts.py +494 -0
  159. dataface/core/compile/normalize_layout.py +1000 -0
  160. dataface/core/compile/normalize_queries.py +297 -0
  161. dataface/core/compile/normalize_variables.py +489 -0
  162. dataface/core/compile/normalizer.py +543 -0
  163. dataface/core/compile/palette.py +1100 -0
  164. dataface/core/compile/parameterized.py +658 -0
  165. dataface/core/compile/parser.py +228 -0
  166. dataface/core/compile/schema.py +20 -0
  167. dataface/core/compile/schema_renderers/__init__.py +0 -0
  168. dataface/core/compile/schema_renderers/json_schema.py +163 -0
  169. dataface/core/compile/schema_renderers/prompt.py +152 -0
  170. dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
  171. dataface/core/compile/sizing.py +2126 -0
  172. dataface/core/compile/sources.py +518 -0
  173. dataface/core/compile/sql_authoring_lint.py +56 -0
  174. dataface/core/compile/style_cascade.py +471 -0
  175. dataface/core/compile/typography.py +299 -0
  176. dataface/core/compile/validator.py +301 -0
  177. dataface/core/compile/variables.py +53 -0
  178. dataface/core/compile/vega_config.py +98 -0
  179. dataface/core/compile/vega_lite/__init__.py +6 -0
  180. dataface/core/compile/vega_lite/validation.py +95 -0
  181. dataface/core/compile/yaml_error_formatter.py +838 -0
  182. dataface/core/connections.py +38 -0
  183. dataface/core/dashboard.py +358 -0
  184. dataface/core/defaults/default_config.yml +101 -0
  185. dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
  186. dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
  187. dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
  188. dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
  189. dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
  190. dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
  191. dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
  192. dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
  193. dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
  194. dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
  195. dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
  196. dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
  197. dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
  198. dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
  199. dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
  200. dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
  201. dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
  202. dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
  203. dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
  204. dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
  205. dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
  206. dataface/core/defaults/palettes/sequential/amber.yml +22 -0
  207. dataface/core/defaults/palettes/sequential/blue.yml +22 -0
  208. dataface/core/defaults/palettes/sequential/brown.yml +22 -0
  209. dataface/core/defaults/palettes/sequential/gray.yml +22 -0
  210. dataface/core/defaults/palettes/sequential/green.yml +22 -0
  211. dataface/core/defaults/palettes/sequential/purple.yml +22 -0
  212. dataface/core/defaults/palettes/sequential/rust.yml +22 -0
  213. dataface/core/defaults/palettes/sequential/teal.yml +22 -0
  214. dataface/core/defaults/palettes/tone/negative.yml +32 -0
  215. dataface/core/defaults/palettes/tone/positive.yml +22 -0
  216. dataface/core/defaults/palettes/tone/warning.yml +22 -0
  217. dataface/core/defaults/themes/_base.yaml +786 -0
  218. dataface/core/defaults/themes/bi.yaml +16 -0
  219. dataface/core/defaults/themes/carbong100.yaml +41 -0
  220. dataface/core/defaults/themes/cream.yaml +122 -0
  221. dataface/core/defaults/themes/dark.yaml +40 -0
  222. dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
  223. dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
  224. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
  225. dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
  226. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
  227. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
  228. dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
  229. dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
  230. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
  231. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
  232. dataface/core/defaults/themes/editorial.yaml +147 -0
  233. dataface/core/defaults/themes/light.yaml +30 -0
  234. dataface/core/defaults/themes/looker.yaml +17 -0
  235. dataface/core/defaults/themes/stark.yaml +134 -0
  236. dataface/core/errors/__init__.py +67 -0
  237. dataface/core/errors/codes_compile.py +56 -0
  238. dataface/core/errors/codes_execute.py +177 -0
  239. dataface/core/errors/codes_render.py +106 -0
  240. dataface/core/errors/codes_unknown.py +15 -0
  241. dataface/core/errors/hints.py +74 -0
  242. dataface/core/errors/registry.py +42 -0
  243. dataface/core/errors/structured.py +92 -0
  244. dataface/core/execute/__init__.py +91 -0
  245. dataface/core/execute/adapters/__init__.py +49 -0
  246. dataface/core/execute/adapters/adapter_registry.py +400 -0
  247. dataface/core/execute/adapters/base.py +245 -0
  248. dataface/core/execute/adapters/csv_adapter.py +239 -0
  249. dataface/core/execute/adapters/dbt_adapter.py +283 -0
  250. dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
  251. dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
  252. dataface/core/execute/adapters/dbt_utils.py +150 -0
  253. dataface/core/execute/adapters/http_adapter.py +224 -0
  254. dataface/core/execute/adapters/metricflow_adapter.py +94 -0
  255. dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
  256. dataface/core/execute/adapters/sql_adapter.py +710 -0
  257. dataface/core/execute/adapters/values_adapter.py +58 -0
  258. dataface/core/execute/batch.py +744 -0
  259. dataface/core/execute/cache_backend.py +135 -0
  260. dataface/core/execute/cache_keys.py +66 -0
  261. dataface/core/execute/dbt_jinja.py +21 -0
  262. dataface/core/execute/dialects/__init__.py +121 -0
  263. dataface/core/execute/dialects/athena.py +75 -0
  264. dataface/core/execute/dialects/base.py +302 -0
  265. dataface/core/execute/dialects/bigquery.py +38 -0
  266. dataface/core/execute/dialects/databricks.py +68 -0
  267. dataface/core/execute/dialects/duckdb.py +35 -0
  268. dataface/core/execute/dialects/mysql.py +68 -0
  269. dataface/core/execute/dialects/postgres.py +39 -0
  270. dataface/core/execute/dialects/redshift.py +12 -0
  271. dataface/core/execute/dialects/snowflake.py +51 -0
  272. dataface/core/execute/dialects/sqlserver.py +92 -0
  273. dataface/core/execute/duckdb_cache.py +712 -0
  274. dataface/core/execute/duckdb_config.py +26 -0
  275. dataface/core/execute/errors.py +213 -0
  276. dataface/core/execute/executor.py +1249 -0
  277. dataface/core/execute/parallel.py +162 -0
  278. dataface/core/execute/setup_sql.py +58 -0
  279. dataface/core/execute/source_registry.py +72 -0
  280. dataface/core/execute/source_resolver.py +255 -0
  281. dataface/core/execute/sql_guard.py +387 -0
  282. dataface/core/execute/sql_literals.py +199 -0
  283. dataface/core/fonts.py +52 -0
  284. dataface/core/inspect/__init__.py +32 -0
  285. dataface/core/inspect/cache_factory.py +98 -0
  286. dataface/core/inspect/db_types.py +162 -0
  287. dataface/core/inspect/dbt_schema.py +96 -0
  288. dataface/core/inspect/defaults.yml +37 -0
  289. dataface/core/inspect/fanout_risk.py +109 -0
  290. dataface/core/inspect/manifest_utils.py +77 -0
  291. dataface/core/inspect/partials/categorical.yml +40 -0
  292. dataface/core/inspect/partials/date.yml +40 -0
  293. dataface/core/inspect/partials/numeric.yml +55 -0
  294. dataface/core/inspect/partition_types.py +38 -0
  295. dataface/core/inspect/query_validator.py +975 -0
  296. dataface/core/inspect/renderer.py +354 -0
  297. dataface/core/inspect/resolver.py +808 -0
  298. dataface/core/inspect/search.py +461 -0
  299. dataface/core/inspect/sources/__init__.py +32 -0
  300. dataface/core/inspect/sources/dbt.py +738 -0
  301. dataface/core/inspect/sources/duckdb_utils.py +66 -0
  302. dataface/core/inspect/templates/__init__.py +1 -0
  303. dataface/core/inspect/templates/categorical_column.yml +196 -0
  304. dataface/core/inspect/templates/charts.yml +109 -0
  305. dataface/core/inspect/templates/date_column.yml +248 -0
  306. dataface/core/inspect/templates/model.yml +138 -0
  307. dataface/core/inspect/templates/numeric_column.yml +261 -0
  308. dataface/core/inspect/templates/quality.yml +80 -0
  309. dataface/core/inspect/templates/string_column.yml +263 -0
  310. dataface/core/project_roots.py +165 -0
  311. dataface/core/render/__init__.py +87 -0
  312. dataface/core/render/board_links.py +176 -0
  313. dataface/core/render/chart/__init__.py +27 -0
  314. dataface/core/render/chart/arc_attached_table.py +251 -0
  315. dataface/core/render/chart/artifacts.py +16 -0
  316. dataface/core/render/chart/callout.py +225 -0
  317. dataface/core/render/chart/decisions.py +358 -0
  318. dataface/core/render/chart/geo.py +700 -0
  319. dataface/core/render/chart/kpi.py +916 -0
  320. dataface/core/render/chart/labels.py +76 -0
  321. dataface/core/render/chart/pipeline.py +818 -0
  322. dataface/core/render/chart/presentation.py +36 -0
  323. dataface/core/render/chart/profile.py +3438 -0
  324. dataface/core/render/chart/render_single.py +347 -0
  325. dataface/core/render/chart/renderers.py +193 -0
  326. dataface/core/render/chart/rendering.py +565 -0
  327. dataface/core/render/chart/serialization.py +90 -0
  328. dataface/core/render/chart/spark.py +496 -0
  329. dataface/core/render/chart/spark_bar.py +370 -0
  330. dataface/core/render/chart/spec_builders.py +154 -0
  331. dataface/core/render/chart/standard_renderer.py +2645 -0
  332. dataface/core/render/chart/table.py +2957 -0
  333. dataface/core/render/chart/table_support.py +1452 -0
  334. dataface/core/render/chart/tick_values.py +66 -0
  335. dataface/core/render/chart/time_unit_detect.py +809 -0
  336. dataface/core/render/chart/title_overflow.py +157 -0
  337. dataface/core/render/chart/type_inference.py +122 -0
  338. dataface/core/render/chart/validation.py +99 -0
  339. dataface/core/render/chart/vega_lite.py +125 -0
  340. dataface/core/render/chart/vega_lite_types.py +268 -0
  341. dataface/core/render/chart/vl_field_maps.py +346 -0
  342. dataface/core/render/chart_interactivity.py +24 -0
  343. dataface/core/render/control_registry.py +287 -0
  344. dataface/core/render/converters/__init__.py +24 -0
  345. dataface/core/render/converters/chart.py +276 -0
  346. dataface/core/render/converters/html.py +98 -0
  347. dataface/core/render/converters/pdf.py +40 -0
  348. dataface/core/render/converters/png.py +41 -0
  349. dataface/core/render/errors.py +144 -0
  350. dataface/core/render/face_api.py +160 -0
  351. dataface/core/render/faces.py +1194 -0
  352. dataface/core/render/font_measurement.py +48 -0
  353. dataface/core/render/font_support.py +197 -0
  354. dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
  355. dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
  356. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
  357. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
  358. dataface/core/render/fonts/InterVariable.ttf +0 -0
  359. dataface/core/render/fonts/InterVariable.woff2 +0 -0
  360. dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
  361. dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
  362. dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
  363. dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
  364. dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
  365. dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
  366. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
  367. dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
  368. dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
  369. dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
  370. dataface/core/render/fonts/_emoji_font_face.css +43 -0
  371. dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
  372. dataface/core/render/format_utils.py +329 -0
  373. dataface/core/render/geo_defaults.yml +28 -0
  374. dataface/core/render/json_format.py +146 -0
  375. dataface/core/render/layout_sizing.py +865 -0
  376. dataface/core/render/layouts.py +541 -0
  377. dataface/core/render/markdown_defaults.yml +16 -0
  378. dataface/core/render/missing_vars_prompt.py +79 -0
  379. dataface/core/render/placeholder.py +389 -0
  380. dataface/core/render/render_result.py +14 -0
  381. dataface/core/render/renderer.py +467 -0
  382. dataface/core/render/script_embedding.py +16 -0
  383. dataface/core/render/svg_utils.py +212 -0
  384. dataface/core/render/template_loader.py +69 -0
  385. dataface/core/render/templates/controls/_styles.css +606 -0
  386. dataface/core/render/templates/controls/checkbox.html +16 -0
  387. dataface/core/render/templates/controls/date.html +16 -0
  388. dataface/core/render/templates/controls/number.html +19 -0
  389. dataface/core/render/templates/controls/readonly.html +9 -0
  390. dataface/core/render/templates/controls/select.html +21 -0
  391. dataface/core/render/templates/controls/slider.html +22 -0
  392. dataface/core/render/templates/controls/text.html +16 -0
  393. dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
  394. dataface/core/render/templates/scripts/variables.js +976 -0
  395. dataface/core/render/templates/svg/grid_pattern.svg +3 -0
  396. dataface/core/render/templates/svg/styles.css +51 -0
  397. dataface/core/render/terminal.py +311 -0
  398. dataface/core/render/terminal_charts.py +563 -0
  399. dataface/core/render/terminal_defaults.yml +2 -0
  400. dataface/core/render/terminal_layouts.py +299 -0
  401. dataface/core/render/terminal_text.py +31 -0
  402. dataface/core/render/text/__init__.py +1 -0
  403. dataface/core/render/text/case.py +113 -0
  404. dataface/core/render/text_format.py +129 -0
  405. dataface/core/render/utils.py +106 -0
  406. dataface/core/render/variable_controls.py +946 -0
  407. dataface/core/render/variable_input_refinement.py +140 -0
  408. dataface/core/render/warnings/__init__.py +15 -0
  409. dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
  410. dataface/core/render/warnings/base.py +44 -0
  411. dataface/core/render/warnings/fanout_risk.py +15 -0
  412. dataface/core/render/warnings/from_query_diagnostic.py +56 -0
  413. dataface/core/render/warnings/missing_join_predicate.py +13 -0
  414. dataface/core/render/warnings/query_parse_error.py +14 -0
  415. dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
  416. dataface/core/render/warnings/reaggregation.py +14 -0
  417. dataface/core/render/warnings/registry.py +45 -0
  418. dataface/core/render/warnings/suppression.py +46 -0
  419. dataface/core/render/warnings/temporal_single_point.py +63 -0
  420. dataface/core/render/warnings/unreferenced_chart.py +15 -0
  421. dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
  422. dataface/core/render/yaml_format.py +167 -0
  423. dataface/core/resolve_face.py +195 -0
  424. dataface/core/schema/__init__.py +0 -0
  425. dataface/core/schema/guidance.py +151 -0
  426. dataface/core/scoped_paths.py +59 -0
  427. dataface/core/serve/__init__.py +14 -0
  428. dataface/core/serve/bootstrap.py +39 -0
  429. dataface/core/serve/embedded.py +57 -0
  430. dataface/core/serve/port.py +129 -0
  431. dataface/core/serve/server.py +938 -0
  432. dataface/core/serve/templates/__init__.py +0 -0
  433. dataface/core/serve/templates/directory.yml +6 -0
  434. dataface/core/serve/templates/error.html.j2 +217 -0
  435. dataface/core/utils.py +121 -0
  436. dataface/core/validate.py +64 -0
  437. dataface/integrations/__init__.py +0 -0
  438. dataface/integrations/highlighting.py +351 -0
  439. dataface/integrations/markdown.py +537 -0
  440. dataface/py.typed +0 -0
  441. dataface-0.1.2.dist-info/METADATA +375 -0
  442. dataface-0.1.2.dist-info/RECORD +455 -0
  443. dataface-0.1.2.dist-info/WHEEL +4 -0
  444. dataface-0.1.2.dist-info/entry_points.txt +2 -0
  445. dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
  446. mdsvg/__init__.py +168 -0
  447. mdsvg/fonts.py +656 -0
  448. mdsvg/images.py +299 -0
  449. mdsvg/parser.py +629 -0
  450. mdsvg/playground.py +284 -0
  451. mdsvg/py.typed +2 -0
  452. mdsvg/renderer.py +1623 -0
  453. mdsvg/style.py +355 -0
  454. mdsvg/types.py +200 -0
  455. mdsvg/utils.py +86 -0
@@ -0,0 +1,645 @@
1
+ """Terminal chat CLI (`dft chat`)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import json
7
+ import re
8
+ import shutil
9
+ import signal
10
+ import tempfile
11
+ import time
12
+ from collections.abc import Callable
13
+ from contextlib import AbstractContextManager, nullcontext
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
17
+
18
+ if TYPE_CHECKING:
19
+ from dataface.ai.external_mcp import ExternalMCPManager
20
+
21
+ import typer
22
+ from rich.console import Console
23
+ from rich.panel import Panel
24
+ from rich.syntax import Syntax
25
+ from rich.table import Table
26
+
27
+ from dataface.agent_api import chat as _api
28
+ from dataface.agent_api._state import dft_home
29
+ from dataface.agent_api.file_refs import expand_file_refs
30
+ from dataface.ai.context import DatafaceAIContext
31
+ from dataface.ai.events import (
32
+ AgentDone,
33
+ AgentError,
34
+ ContentDelta,
35
+ ThinkingStatus,
36
+ ToolCallEvent,
37
+ ToolResultEvent,
38
+ )
39
+ from dataface.ai.llm import create_client, infer_provider
40
+ from dataface.cli.commands._agent_input import (
41
+ PromptToolkitInput,
42
+ select_input_layer,
43
+ )
44
+
45
+ # Max JSON lines before we truncate and write the full result to a tmpfile.
46
+ _JSON_TRUNCATE_LINES = 80
47
+
48
+ # Completed stream status — used by _run_agent_session callers.
49
+ _SessionStatus = Literal["done", "interrupted", "errored"]
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Interrupt sentinel — BaseException so it bypasses 'except Exception' in tools
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ class _AgentInterrupt(BaseException):
58
+ """Raised by the SIGINT handler to cancel an in-flight LLM/tool call."""
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Session dataclass (CLI REPL state — holds console, not persisted)
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ @dataclass
67
+ class Session:
68
+ """Mutable REPL state for one dft chat session."""
69
+
70
+ client: Any
71
+ context: DatafaceAIContext
72
+ console: Console
73
+ messages: list[dict[str, Any]] = field(default_factory=list)
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Slash-command helpers
78
+ # ---------------------------------------------------------------------------
79
+
80
+
81
+ def _cmd_exit(session: Session, args: list[str]) -> None:
82
+ session.console.print("[dim]Session ended.[/dim]")
83
+ raise SystemExit(0)
84
+
85
+
86
+ def _cmd_clear(session: Session, args: list[str]) -> None:
87
+ session.messages.clear()
88
+ # Rebuild the client to reset any provider-side conversation state
89
+ # (e.g. OpenAIClient._previous_response_id / _sent_message_count).
90
+ # Without this, OpenAI's incremental mode would compute an empty input
91
+ # slice on the next prompt and silently return nothing.
92
+ session.client = create_client(model=session.client.model)
93
+ session.console.print("[dim]Conversation history cleared.[/dim]")
94
+
95
+
96
+ def _cmd_model(session: Session, args: list[str]) -> None:
97
+ if not args:
98
+ session.console.print(
99
+ f"[dim]Current model: {session.client.model} "
100
+ f"(provider: {session.client.provider})[/dim]"
101
+ )
102
+ return
103
+ requested_model = args[0]
104
+ requested_provider = infer_provider(requested_model)
105
+ if requested_provider != session.client.provider:
106
+ session.console.print(
107
+ f"[red]/model: cannot switch providers mid-session "
108
+ f"(current: {session.client.provider}, requested: {requested_provider}). "
109
+ f"Restart dft chat with --model {requested_model}.[/red]"
110
+ )
111
+ return
112
+ new_client = create_client(model=requested_model)
113
+ session.client = new_client
114
+ # Clear messages: the new client has no previous_response_id chain,
115
+ # so prior tool-call history would cause LLMClientError on OpenAI.
116
+ session.messages.clear()
117
+ session.console.print(
118
+ f"[dim]Switched to model: {new_client.model} (history cleared)[/dim]"
119
+ )
120
+
121
+
122
+ def _cmd_help(session: Session, args: list[str]) -> None:
123
+ lines = [
124
+ "[bold]/help[/bold] — show this help",
125
+ "[bold]/clear[/bold] — drop conversation history (keep session alive)",
126
+ "[bold]/model[/bold] [name] — show current model or switch to another (same provider only)",
127
+ "[bold]/exit[/bold] — end the session",
128
+ "[bold]/quit[/bold] — alias for /exit",
129
+ "",
130
+ "Ctrl+C — cancel in-flight request; second Ctrl+C within 1s exits",
131
+ "Ctrl+D — exit session",
132
+ ]
133
+ session.console.print("\n".join(lines))
134
+
135
+
136
+ SLASH_COMMANDS: dict[str, Callable[[Session, list[str]], None]] = {
137
+ "exit": _cmd_exit,
138
+ "quit": _cmd_exit,
139
+ "clear": _cmd_clear,
140
+ "model": _cmd_model,
141
+ "help": _cmd_help,
142
+ }
143
+
144
+
145
+ def _dispatch_slash(session: Session, user_input: str) -> bool:
146
+ """Try to dispatch a slash command.
147
+
148
+ Returns True if the input was consumed as a slash command, False if the
149
+ input should be sent to the model.
150
+
151
+ Unknown slash commands pass through to the model verbatim (per spec: "no
152
+ magic").
153
+ """
154
+ stripped = user_input.strip()
155
+ if not stripped.startswith("/"):
156
+ return False
157
+ parts = stripped[1:].split()
158
+ if not parts:
159
+ return False
160
+ cmd_name = parts[0].lower()
161
+ cmd_args = parts[1:]
162
+ handler = SLASH_COMMANDS.get(cmd_name)
163
+ if handler is None:
164
+ return False
165
+ handler(session, cmd_args)
166
+ return True
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # Ctrl+C double-tap-exit helpers
171
+ # ---------------------------------------------------------------------------
172
+
173
+
174
+ def _check_double_tap_exit(last_sigint_ts: list[float]) -> bool:
175
+ """Record an interrupt timestamp and return True if it's a double-tap.
176
+
177
+ Args:
178
+ last_sigint_ts: A mutable list used as a single-slot timestamp store.
179
+ Pass an empty list on first call; the function appends/updates it.
180
+
181
+ Returns:
182
+ True if a second interrupt arrived within 1s of the previous one.
183
+ """
184
+ now = time.monotonic()
185
+ if last_sigint_ts and (now - last_sigint_ts[0]) < 1.0:
186
+ return True
187
+ if last_sigint_ts:
188
+ last_sigint_ts[0] = now
189
+ else:
190
+ last_sigint_ts.append(now)
191
+ return False
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # Tool-result rendering
196
+ # ---------------------------------------------------------------------------
197
+
198
+
199
+ def _render_tool_result(name: str, result: Any, console: Console, tmpdir: Path) -> None:
200
+ """Render a tool result to the console.
201
+
202
+ Dispatch:
203
+ - Tabular execute_query result → rich.Table
204
+ - Error dict → red Panel
205
+ - Oversized JSON → truncated Panel with tmpfile reference
206
+ - Default → JSON Syntax Panel
207
+
208
+ tmpdir is a session-scoped directory for oversized-result files; the
209
+ caller cleans it up at session end.
210
+ """
211
+ # Tabular: execute_query returns {success, data: list[dict], columns, ...}
212
+ if (
213
+ isinstance(result, dict)
214
+ and isinstance(result.get("data"), list)
215
+ and isinstance(result.get("columns"), list)
216
+ and result.get("columns")
217
+ and not result.get("error")
218
+ ):
219
+ _render_tabular(name, result, console)
220
+ return
221
+
222
+ # Error dict
223
+ if isinstance(result, dict) and result.get("error"):
224
+ console.print(
225
+ Panel(
226
+ f"[red]{result['error']}[/red]",
227
+ title=f"Tool Error: {name}",
228
+ border_style="red",
229
+ )
230
+ )
231
+ return
232
+
233
+ # Default: JSON, possibly truncated
234
+ body = json.dumps(result, indent=2, default=str, sort_keys=True)
235
+ lines = body.splitlines()
236
+ if len(lines) > _JSON_TRUNCATE_LINES:
237
+ truncated_body = "\n".join(lines[:_JSON_TRUNCATE_LINES])
238
+ extra = len(lines) - _JSON_TRUNCATE_LINES
239
+ # Write the full result under the session tmpdir so it's cleaned up on exit.
240
+ # Sanitize name to avoid path separators from LLM-provided tool names.
241
+ safe_name = re.sub(r"[^\w-]", "_", name)
242
+ tmp_file = tmpdir / f"dft_{safe_name}_{int(time.monotonic() * 1000)}.json"
243
+ tmp_file.write_text(body)
244
+ summary = f"[dim]... ({extra} more lines, full result in {tmp_file})[/dim]"
245
+ console.print(
246
+ Panel(
247
+ Syntax(truncated_body, "json", word_wrap=True),
248
+ title=f"Tool Result: {name}",
249
+ border_style="blue",
250
+ subtitle=summary,
251
+ )
252
+ )
253
+ else:
254
+ console.print(
255
+ Panel(
256
+ Syntax(body, "json", word_wrap=True),
257
+ title=f"Tool Result: {name}",
258
+ border_style="blue",
259
+ )
260
+ )
261
+
262
+
263
+ def _render_tabular(name: str, result: dict[str, Any], console: Console) -> None:
264
+ columns: list[str] = result["columns"]
265
+ data: list[dict[str, Any]] = result["data"]
266
+
267
+ table = Table(title=f"Tool Result: {name}", border_style="blue", show_lines=False)
268
+ for col in columns:
269
+ table.add_column(col, overflow="fold")
270
+ for row in data:
271
+ table.add_row(*[str(row.get(col, "")) for col in columns])
272
+
273
+ console.print(table)
274
+ if result.get("truncated"):
275
+ console.print(f"[dim](results truncated to {len(data)} rows)[/dim]")
276
+
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # Agent stream loop — extracted so tests can call it directly
280
+ # ---------------------------------------------------------------------------
281
+
282
+
283
+ def _run_agent_session(
284
+ chat_session: _api.ChatSession,
285
+ prompt: str,
286
+ console: Console,
287
+ tmpdir: Path,
288
+ tools: list[dict[str, Any]] | None = None,
289
+ external_manager: ExternalMCPManager | None = None,
290
+ ) -> _SessionStatus:
291
+ """Stream one prompt's events through ``agent_api.chat.send_message``.
292
+
293
+ Installs a SIGINT handler for the duration that raises _AgentInterrupt.
294
+ Persistence is handled inside ``send_message`` (its ``try/finally`` writes
295
+ the turn to disk even when the generator is closed mid-stream).
296
+
297
+ Args:
298
+ tools: Merged tool list (built-in + external). Defaults to ALL_TOOLS.
299
+ external_manager: ExternalMCPManager for routing 'server__tool' calls.
300
+
301
+ Returns:
302
+ "done" — completed normally
303
+ "interrupted" — cancelled by Ctrl+C
304
+ "errored" — AgentError received (message already printed in red)
305
+ """
306
+ started_content = False
307
+ status: _SessionStatus = "done"
308
+
309
+ def _sigint_handler(_signum: int, _frame: Any) -> None:
310
+ raise _AgentInterrupt()
311
+
312
+ prev_handler = signal.signal(signal.SIGINT, _sigint_handler)
313
+
314
+ gen = _api.send_message(
315
+ chat_session,
316
+ prompt,
317
+ tools=tools,
318
+ external_manager=external_manager,
319
+ )
320
+ try:
321
+ for event in gen:
322
+ match event:
323
+ case ThinkingStatus(status=thk_status):
324
+ if started_content:
325
+ console.print()
326
+ started_content = False
327
+ console.print(f"[dim]{thk_status}[/dim]")
328
+ case ToolCallEvent(name=name, arguments=args):
329
+ if started_content:
330
+ console.print()
331
+ started_content = False
332
+ console.print(
333
+ f"[bold cyan]Tool[/bold cyan] {name} "
334
+ f"[dim]{json.dumps(args, default=str)}[/dim]"
335
+ )
336
+ case ToolResultEvent(name=name, result=result):
337
+ if started_content:
338
+ console.print()
339
+ started_content = False
340
+ _render_tool_result(name, result, console, tmpdir)
341
+ case ContentDelta(delta=delta):
342
+ if not started_content:
343
+ console.print("[bold green]Assistant[/bold green]", end=" ")
344
+ started_content = True
345
+ console.out(delta, end="")
346
+ case AgentError(message=msg):
347
+ if started_content:
348
+ console.print()
349
+ started_content = False
350
+ console.print(f"[red]{msg}[/red]")
351
+ status = "errored"
352
+ return status
353
+ case AgentDone():
354
+ pass
355
+ except _AgentInterrupt:
356
+ status = "interrupted"
357
+ gen.close() # closes HTTP connection via generator protocol
358
+ finally:
359
+ signal.signal(signal.SIGINT, prev_handler)
360
+ if started_content:
361
+ console.print()
362
+
363
+ if status == "interrupted":
364
+ console.print("[dim]interrupted[/dim]")
365
+
366
+ return status
367
+
368
+
369
+ # ---------------------------------------------------------------------------
370
+ # Resume helpers
371
+ # ---------------------------------------------------------------------------
372
+
373
+
374
+ def _pick_session_interactive(console: Console) -> str | None:
375
+ """Show a rich.Table of recent sessions and prompt for a selection.
376
+
377
+ Returns the selected session_id, or None if the user aborts.
378
+ """
379
+ summaries = _api.list_sessions()
380
+ recent = summaries[:10]
381
+
382
+ if not recent:
383
+ console.print("[dim]No saved sessions found.[/dim]")
384
+ return None
385
+
386
+ table = Table(title="Recent sessions", border_style="dim", show_lines=False)
387
+ table.add_column("#", style="dim", width=3)
388
+ table.add_column("Started", style="cyan")
389
+ table.add_column("CWD", style="green")
390
+ table.add_column("ID", style="dim")
391
+ for i, entry in enumerate(recent, 1):
392
+ table.add_row(
393
+ str(i),
394
+ entry.started_at[:19],
395
+ str(entry.cwd),
396
+ entry.session_id[:8],
397
+ )
398
+ console.print(table)
399
+
400
+ raw = typer.prompt("Select session number (Enter to cancel)", default="")
401
+ if not raw.strip():
402
+ return None
403
+ try:
404
+ idx_choice = int(raw.strip()) - 1
405
+ if not (0 <= idx_choice < len(recent)):
406
+ raise ValueError
407
+ return recent[idx_choice].session_id
408
+ except ValueError:
409
+ console.print("[red]Invalid selection.[/red]")
410
+ return None
411
+
412
+
413
+ def _resolve_resume_id(
414
+ continue_session: bool,
415
+ resume: str | None,
416
+ pick: bool,
417
+ cwd: Path,
418
+ console: Console,
419
+ ) -> str | None:
420
+ """Return the session_id to resume, or None for a fresh session."""
421
+ if resume is not None:
422
+ return resume.strip()
423
+ if pick:
424
+ return _pick_session_interactive(console)
425
+ if continue_session:
426
+ summaries = _api.list_sessions(owner=cwd)
427
+ if not summaries:
428
+ console.print(
429
+ "[dim]No prior session found for this directory. "
430
+ "Starting fresh.[/dim]"
431
+ )
432
+ return None
433
+ return summaries[0].session_id
434
+ return None
435
+
436
+
437
+ # ---------------------------------------------------------------------------
438
+ # Public command
439
+ # ---------------------------------------------------------------------------
440
+
441
+
442
+ def chat_command(
443
+ prompt: Annotated[
444
+ str | None,
445
+ typer.Argument(help="Optional one-shot prompt"),
446
+ ] = None,
447
+ model: Annotated[
448
+ str | None,
449
+ typer.Option("--model", help="Model name, optionally provider-prefixed"),
450
+ ] = None,
451
+ continue_session: Annotated[
452
+ bool,
453
+ typer.Option(
454
+ "-c",
455
+ "--continue",
456
+ help="Resume the most recent session for the current directory",
457
+ ),
458
+ ] = False,
459
+ resume: Annotated[
460
+ str | None,
461
+ typer.Option(
462
+ "-r",
463
+ "--resume",
464
+ help="Resume a specific session by id (see --pick for the interactive list)",
465
+ ),
466
+ ] = None,
467
+ pick: Annotated[
468
+ bool,
469
+ typer.Option(
470
+ "-p",
471
+ "--pick",
472
+ help="Open an interactive picker to choose a session to resume",
473
+ ),
474
+ ] = False,
475
+ ) -> None:
476
+ """Chat with a terminal AI agent.
477
+
478
+ Sessions are auto-saved to ~/.dft/sessions/ and indexed by working directory.
479
+
480
+ Examples:
481
+ dft chat -c Resume last session for this directory
482
+ dft chat -r <session-id> Resume a specific session by id
483
+ dft chat -p Pick from a list of recent sessions
484
+
485
+ External MCP servers are auto-loaded from the same config files that
486
+ `dft init mcp` writes (.cursor/mcp.json, .mcp.json, etc.). Dataface's own
487
+ MCP server entry is skipped — in-process tools are used instead.
488
+ External tools are namespaced as `server__tool` (double underscore, matching
489
+ Claude Code's MCP convention and compatible with LLM provider tool-name rules).
490
+ """
491
+ from dataface.cli._extras import require_extras
492
+
493
+ require_extras("chat")
494
+
495
+ import os
496
+
497
+ from dataface.ai.external_mcp import ExternalMCPManager, discover_servers
498
+ from dataface.ai.tool_schemas import ALL_TOOLS
499
+ from dataface.cli.commands._agent_server import (
500
+ start_http_server,
501
+ stop_http_server,
502
+ )
503
+
504
+ console = Console()
505
+
506
+ # Start the embedded preview HTTP server before building context, so
507
+ # render_dashboard URLs point at the agent's own server.
508
+ server_port_hint = int(os.getenv("DFT_CHAT_PORT", "8765"))
509
+ http_handle = start_http_server(port_hint=server_port_hint)
510
+ console.print(f"[dim]Preview server: http://localhost:{http_handle.port}/[/dim]")
511
+
512
+ cwd = Path.cwd().resolve()
513
+ session_id_to_resume = _resolve_resume_id(
514
+ continue_session, resume, pick, cwd, console
515
+ )
516
+
517
+ chat_session: _api.ChatSession | None = None
518
+ external_manager: ExternalMCPManager | None = None
519
+ try:
520
+ if session_id_to_resume:
521
+ try:
522
+ chat_session = _api.resume_session(
523
+ session_id_to_resume,
524
+ project_dir=Path.cwd(),
525
+ server_port=http_handle.port,
526
+ model=model,
527
+ )
528
+ console.print(
529
+ f"[dim]Resumed session {session_id_to_resume[:8]} "
530
+ f"({len(chat_session.messages)} messages)[/dim]"
531
+ )
532
+ except ValueError as exc:
533
+ console.print(f"[red]{exc}[/red]")
534
+ raise typer.Exit(1) from exc
535
+ else:
536
+ chat_session = _api.start_session(
537
+ model=model,
538
+ project_dir=Path.cwd(),
539
+ server_port=http_handle.port,
540
+ )
541
+
542
+ external_configs = discover_servers()
543
+ external_manager = ExternalMCPManager(external_configs)
544
+ external_manager.start(timeout=10.0)
545
+ merged_tools = ALL_TOOLS + external_manager.all_tools()
546
+
547
+ session = Session(
548
+ client=chat_session.client,
549
+ context=chat_session.context,
550
+ console=console,
551
+ )
552
+ session.messages = chat_session.messages
553
+
554
+ if prompt:
555
+ tmpdir = Path(tempfile.mkdtemp(prefix="dft_chat_"))
556
+ atexit.register(shutil.rmtree, str(tmpdir), True)
557
+ result_status = _run_agent_session(
558
+ chat_session,
559
+ prompt,
560
+ console=console,
561
+ tmpdir=tmpdir,
562
+ tools=merged_tools,
563
+ external_manager=external_manager,
564
+ )
565
+ if result_status == "interrupted":
566
+ raise typer.Exit(130)
567
+ if result_status == "errored":
568
+ raise typer.Exit(1)
569
+ return
570
+
571
+ _run_interactive(
572
+ session=session,
573
+ chat_session=chat_session,
574
+ console=console,
575
+ merged_tools=merged_tools,
576
+ external_manager=external_manager,
577
+ )
578
+ finally:
579
+ if external_manager is not None:
580
+ external_manager.close()
581
+ stop_http_server(http_handle)
582
+ if chat_session is not None:
583
+ chat_session.writer.close()
584
+
585
+
586
+ def _run_interactive(
587
+ session: Session,
588
+ chat_session: _api.ChatSession,
589
+ console: Console,
590
+ merged_tools: list[dict[str, Any]],
591
+ external_manager: ExternalMCPManager,
592
+ ) -> None:
593
+ """Run the interactive REPL loop."""
594
+ tmpdir = Path(tempfile.mkdtemp(prefix="dft_chat_"))
595
+ atexit.register(shutil.rmtree, str(tmpdir), True)
596
+
597
+ hist_path = dft_home() / "chat_history"
598
+ input_layer = select_input_layer(hist_path, slash_commands=set(SLASH_COMMANDS))
599
+ stream_ctx: Callable[..., AbstractContextManager[None]]
600
+ if isinstance(input_layer, PromptToolkitInput):
601
+ from prompt_toolkit.patch_stdout import (
602
+ patch_stdout as _patch_stdout,
603
+ ) # noqa: PLC0415
604
+
605
+ stream_ctx = _patch_stdout
606
+ else:
607
+ stream_ctx = nullcontext
608
+
609
+ last_sigint_ts: list[float] = []
610
+
611
+ def _at_prompt_sigint(_signum: int, _frame: Any) -> None:
612
+ if _check_double_tap_exit(last_sigint_ts):
613
+ console.print("\n[dim]Session ended.[/dim]")
614
+ raise SystemExit(0)
615
+ console.print("\n[dim](Press Ctrl+C again within 1s to exit, or Ctrl+D)[/dim]")
616
+
617
+ signal.signal(signal.SIGINT, _at_prompt_sigint)
618
+
619
+ while True:
620
+ try:
621
+ user_input = input_layer.read("You: ")
622
+ except EOFError:
623
+ console.print("\n[dim]Session ended.[/dim]")
624
+ return
625
+
626
+ if not user_input.strip():
627
+ continue
628
+ if _dispatch_slash(session, user_input):
629
+ continue
630
+
631
+ user_input = expand_file_refs(user_input, project_dir=Path.cwd()).text
632
+
633
+ # Slash commands like /clear and /model rebuild session.client; mirror
634
+ # that onto chat_session so send_message picks up the new client.
635
+ chat_session.client = session.client
636
+
637
+ with stream_ctx():
638
+ _run_agent_session(
639
+ chat_session,
640
+ user_input,
641
+ console=session.console,
642
+ tmpdir=tmpdir,
643
+ tools=merged_tools,
644
+ external_manager=external_manager,
645
+ )