@hncode/hncode 0.1.8 → 0.1.9
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.
- package/README.md +87 -198
- package/README_CN.md +87 -187
- package/package.json +1 -1
- package/src/agent.js +13 -22
- package/src/clipboard.js +116 -13
- package/src/colors.js +16 -8
- package/src/index.js +1 -1
- package/src/tui.js +200 -169
package/src/tui.js
CHANGED
|
@@ -20,7 +20,7 @@ import cp from 'node:child_process';
|
|
|
20
20
|
import {
|
|
21
21
|
C, blockCursor, showCursor, hideCursor, alternateScreen, clearScreen, clearAndSetBg, setTheme, THEME_NAMES, lerpColor,
|
|
22
22
|
} from './colors.js';
|
|
23
|
-
import { copyText, readText, warmClipboard,
|
|
23
|
+
import { copyText, readText, warmClipboard, readClipboardContentAsync } from './clipboard.js';
|
|
24
24
|
import { visualWidth, estimateTokens, estimateMessagesTokens, expandTabs } from './term.js';
|
|
25
25
|
import { Agent, SYSTEM_PROMPT } from './agent.js';
|
|
26
26
|
import { LLM } from './llm.js';
|
|
@@ -60,7 +60,6 @@ export const COMMANDS = [
|
|
|
60
60
|
{ name: 'usage', desc: 'Show session tokens + context window', priority: 60 },
|
|
61
61
|
{ name: 'mcp', desc: 'Show MCP server status', priority: 60 },
|
|
62
62
|
{ name: 'mcp-config', desc: 'Configure MCP servers (list / add / remove)', priority: 60 },
|
|
63
|
-
{ name: 'theme', desc: 'Set the terminal UI theme', priority: 60, argumentHint: '[auto|dark|light|<name>]' },
|
|
64
63
|
{ name: 'statusline', desc: 'Configure which items appear in the status line', priority: 60 },
|
|
65
64
|
|
|
66
65
|
{ name: 'export-md', aliases: ['export'], desc: 'Export current session as a Markdown file', priority: 40, argumentHint: '[output-path]' },
|
|
@@ -140,7 +139,6 @@ export const TIPS = [
|
|
|
140
139
|
'/compact [ratio] AI-summarizes older history and keeps the most recent 20%',
|
|
141
140
|
'/tasks lists background Bash jobs started with run_in_background',
|
|
142
141
|
'/status prints the current model, provider, endpoint, and session id',
|
|
143
|
-
'/theme switches between the auto / dark / light colour palettes',
|
|
144
142
|
'/statusline toggles which items appear in the bottom status bar',
|
|
145
143
|
'/add-dir grants the agent access to another directory for this session',
|
|
146
144
|
'/reload re-reads config.toml without restarting hncode',
|
|
@@ -210,74 +208,90 @@ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇',
|
|
|
210
208
|
// Rotating status messages for the "Working..." line — all mean "the agent is
|
|
211
209
|
// thinking / working", but vary the wording so it doesn't look stuck.
|
|
212
210
|
const WORKING_MESSAGES = [
|
|
213
|
-
'Thinking
|
|
214
|
-
'
|
|
215
|
-
'
|
|
216
|
-
'
|
|
217
|
-
'
|
|
218
|
-
'
|
|
219
|
-
'
|
|
220
|
-
'
|
|
221
|
-
'
|
|
222
|
-
'
|
|
223
|
-
'
|
|
224
|
-
'
|
|
225
|
-
'
|
|
226
|
-
'
|
|
227
|
-
'
|
|
228
|
-
'
|
|
229
|
-
'
|
|
230
|
-
'
|
|
231
|
-
'
|
|
232
|
-
'
|
|
233
|
-
'
|
|
234
|
-
'
|
|
235
|
-
'
|
|
236
|
-
'
|
|
237
|
-
'
|
|
238
|
-
'
|
|
239
|
-
'
|
|
240
|
-
'
|
|
241
|
-
'
|
|
242
|
-
'
|
|
243
|
-
'
|
|
244
|
-
'
|
|
245
|
-
'
|
|
246
|
-
'
|
|
247
|
-
'
|
|
248
|
-
'
|
|
249
|
-
'
|
|
250
|
-
'
|
|
251
|
-
'
|
|
252
|
-
'
|
|
253
|
-
'
|
|
254
|
-
'
|
|
255
|
-
'
|
|
256
|
-
'
|
|
257
|
-
'
|
|
258
|
-
'
|
|
259
|
-
'
|
|
260
|
-
'
|
|
261
|
-
'
|
|
262
|
-
'
|
|
263
|
-
'
|
|
264
|
-
'
|
|
265
|
-
'
|
|
266
|
-
'
|
|
267
|
-
'
|
|
268
|
-
'
|
|
269
|
-
'
|
|
270
|
-
'
|
|
271
|
-
'
|
|
272
|
-
'
|
|
273
|
-
'
|
|
274
|
-
'
|
|
275
|
-
'
|
|
276
|
-
'
|
|
277
|
-
'
|
|
278
|
-
'
|
|
279
|
-
'
|
|
280
|
-
'
|
|
211
|
+
'Thinking...', 'Working...', 'Crunching...', 'Processing...', 'Analyzing...', 'Pondering...', 'Mulling...', 'Considering...', 'Reasoning...', 'Deliberating...',
|
|
212
|
+
'Reflecting...', 'Ruminating...', 'Planning...', 'Mapping...', 'Sketching...', 'Outlining...', 'Structuring...', 'Organizing...', 'Sorting...', 'Ordering...',
|
|
213
|
+
'Prioritizing...', 'Focusing...', 'Concentrating...', 'Diving...', 'Digging...', 'Delving...', 'Probing...', 'Examining...', 'Inspecting...', 'Studying...',
|
|
214
|
+
'Reviewing...', 'Checking...', 'Verifying...', 'Validating...', 'Confirming...', 'Building...', 'Assembling...', 'Constructing...', 'Composing...', 'Crafting...',
|
|
215
|
+
'Creating...', 'Shaping...', 'Forming...', 'Forging...', 'Welding...', 'Stitching...', 'Weaving...', 'Knitting...', 'Threading...', 'Patching...',
|
|
216
|
+
'Fixing...', 'Repairing...', 'Rebuilding...', 'Reworking...', 'Refactoring...', 'Restructuring...', 'Solving...', 'Untangling...', 'Unraveling...', 'Detangling...',
|
|
217
|
+
'Deciphering...', 'Decoding...', 'Cracking...', 'Piecing...', 'Fitting...', 'Hunting...', 'Chasing...', 'Tracking...', 'Tracing...', 'Sniffing...',
|
|
218
|
+
'Writing...', 'Drafting...', 'Editing...', 'Revising...', 'Rewriting...', 'Proofreading...', 'Polishing...', 'Refining...', 'Honing...', 'Tuning...',
|
|
219
|
+
'Sharpening...', 'Tightening...', 'Trimming...', 'Pruning...', 'Cutting...', 'Merging...', 'Splicing...', 'Searching...', 'Scanning...', 'Sifting...',
|
|
220
|
+
'Filtering...', 'Collecting...', 'Gathering...', 'Compiling...', 'Indexing...', 'Fetching...', 'Retrieving...', 'Loading...', 'Unpacking...', 'Computing...',
|
|
221
|
+
'Calculating...', 'Measuring...', 'Estimating...', 'Calibrating...', 'Aligning...', 'Balancing...', 'Optimizing...', 'Streamlining...', 'Smoothing...', 'Cleaning...',
|
|
222
|
+
'Sweeping...', 'Tidying...', 'Clearing...', 'Expanding...', 'Growing...', 'Blooming...', 'Walking...', 'Strolling...', 'Marching...', 'Hiking...',
|
|
223
|
+
'Trekking...', 'Journeying...', 'Sailing...', 'Cruising...', 'Gliding...', 'Flying...', 'Soaring...', 'Floating...', 'Drifting...', 'Wandering...',
|
|
224
|
+
'Exploring...', 'Roaming...', 'Venturing...', 'Discovering...', 'Uncovering...', 'Unveiling...', 'Revealing...', 'Exposing...', 'Spinning...', 'Whirling...',
|
|
225
|
+
'Twirling...', 'Rotating...', 'Swirling...', 'Churning...', 'Turning...', 'Rolling...', 'Rocking...', 'Swaying...', 'Flowing...', 'Streaming...',
|
|
226
|
+
'Cascading...', 'Rippling...', 'Bubbling...', 'Sparkling...', 'Cooking...', 'Simmering...', 'Boiling...', 'Stewing...', 'Roasting...', 'Baking...',
|
|
227
|
+
'Grilling...', 'Frying...', 'Whisking...', 'Beating...', 'Stirring...', 'Mixing...', 'Blending...', 'Kneading...', 'Proofing...', 'Rising...',
|
|
228
|
+
'Fermenting...', 'Brewing...', 'Steeping...', 'Infusing...', 'Seasoning...', 'Garnishing...', 'Plating...', 'Serving...', 'Tasting...', 'Adjusting...',
|
|
229
|
+
'Perfecting...', 'Glazing...', 'Painting...', 'Drawing...', 'Designing...', 'Orchestrating...', 'Conducting...', 'Dancing...', 'Jigging...', 'Jiving...',
|
|
230
|
+
'Boogying...', 'Shimmying...', 'Grooving...', 'Bouncing...', 'Hopping...', 'Skipping...', 'Nurturing...', 'Tending...', 'Gardening...', 'Planting...',
|
|
231
|
+
'Seeding...', 'Sprouting...', 'Rooting...', 'Branching...', 'Watering...', 'Flourishing...', 'Thriving...', 'Sneaking...', 'Tiptoeing...', 'Creeping...',
|
|
232
|
+
'Charging...', 'Gunning...', 'Ramping...', 'Revving...', 'Warming...', 'Powering...', 'Igniting...', 'Kindling...', 'Sparking...', 'Blazing...',
|
|
233
|
+
'Burning...', 'Glowing...', 'Illuminating...', 'Enlightening...', 'Brightening...', 'Dawning...', 'Breaking...', 'Leaping...', 'Jumping...', 'Bounding...',
|
|
234
|
+
'Springing...', 'Launching...', 'Propelling...', 'Accelerating...', 'Waiting...', 'Holding...', 'Steadying...', 'Progressing...', 'Advancing...', 'Continuing...',
|
|
235
|
+
'Persisting...', 'Persevering...', 'Standing...', 'Readying...', 'Preparing...', 'Sussing...', 'Sleuthing...', 'Orienting...', 'Locating...', 'Targeting...',
|
|
236
|
+
'Acquiring...', 'Syncing...', 'Harmonizing...', 'Lining...', 'Wrapping...', 'Finalizing...', 'Landing...', 'Nailing...', 'Sealing...', 'Rethinking...',
|
|
237
|
+
'Reassessing...', 'Revisiting...', 'Retracing...', 'Replaying...', 'Rehearsing...', 'Simulating...', 'Modeling...', 'Prototyping...', 'Testing...', 'Iterating...',
|
|
238
|
+
'Rendering...', 'Repainting...', 'Compositing...', 'Wiring...', 'Linking...', 'Bundling...', 'Packing...', 'Shipping...', 'Deploying...', 'Juggling...',
|
|
239
|
+
'Weighing...', 'Distilling...', 'Extracting...', 'Parsing...', 'Formatting...', 'Normalizing...', 'Sanitizing...', 'Locking...', 'Securing...', 'Guarding...',
|
|
240
|
+
'Watching...', 'Monitoring...', 'Surveying...', 'Scouting...', 'Reconnoitering...', 'Concocting...', 'Devising...', 'Inventing...', 'Imagining...', 'Envisioning...',
|
|
241
|
+
'Visualizing...', 'Picturing...', 'Dreaming...', 'Wondering...', 'Marveling...', 'Admiring...', 'Appreciating...', 'Enjoying...', 'Savoring...', 'Relishing...',
|
|
242
|
+
'Loving...', 'Cherishing...', 'Treasure-hunting...', 'Questing...', 'Seeking...', 'Striving...', 'Endeavoring...', 'Attempting...', 'Trying...', 'Experimenting...',
|
|
243
|
+
'Innovating...', 'Pioneering...', 'Trailblazing...', 'Pathfinding...', 'Navigating...', 'Steering...', 'Guiding...', 'Directing...', 'Captaining...', 'Piloting...',
|
|
244
|
+
'Commanding...', 'Leading...', 'Spearheading...', 'Championing...', 'Absorbing...', 'Accenting...', 'Acclaiming...', 'Accommodating...', 'Accounting...', 'Accrediting...',
|
|
245
|
+
'Accumulating...', 'Achieving...', 'Acknowledging...', 'Activating...', 'Adapting...', 'Adding...', 'Addressing...', 'Adhering...', 'Administering...', 'Adopting...',
|
|
246
|
+
'Adorning...', 'Advising...', 'Affirming...', 'Aggregating...', 'Agreing...', 'Aiming...', 'Airbrushing...', 'Alerting...', 'Allocating...', 'Allowing...',
|
|
247
|
+
'Altering...', 'Amalgamating...', 'Amending...', 'Amplifying...', 'Amusing...', 'Anchoring...', 'Animating...', 'Annexing...', 'Announcing...', 'Answering...',
|
|
248
|
+
'Anticipating...', 'Appealing...', 'Appending...', 'Applying...', 'Appointing...', 'Appraising...', 'Approaching...', 'Approving...', 'Archiving...', 'Arguing...',
|
|
249
|
+
'Arming...', 'Arranging...', 'Arraying...', 'Arresting...', 'Arriving...', 'Articulating...', 'Ascending...', 'Asserting...', 'Assessing...', 'Assigning...',
|
|
250
|
+
'Assimilating...', 'Assisting...', 'Assuring...', 'Astonishing...', 'Attaching...', 'Attacking...', 'Attaining...', 'Attending...', 'Attesting...', 'Attracting...',
|
|
251
|
+
'Auditing...', 'Augmenting...', 'Authenticating...', 'Authoring...', 'Authorizing...', 'Automating...', 'Averting...', 'Awakening...', 'Awarding...', 'Babbling...',
|
|
252
|
+
'Backing...', 'Backtracking...', 'Badging...', 'Baging...', 'Baiting...', 'Ballooning...', 'Banding...', 'Banking...', 'Bartering...', 'Basing...',
|
|
253
|
+
'Batching...', 'Bearing...', 'Bedazzling...', 'Befriending...', 'Begining...', 'Beholding...', 'Believing...', 'Belonging...', 'Bending...', 'Benefiting...',
|
|
254
|
+
'Beseeching...', 'Bestowing...', 'Beting...', 'Biding...', 'Billowing...', 'Binding...', 'Blasting...', 'Bleaching...', 'Bleeding...', 'Bleeping...',
|
|
255
|
+
'Blessing...', 'Blinking...', 'Blistering...', 'Blocking...', 'Bloting...', 'Blowing...', 'Bluring...', 'Boarding...', 'Boasting...', 'Bobing...',
|
|
256
|
+
'Bolstering...', 'Bombarding...', 'Bonding...', 'Booming...', 'Boosting...', 'Bootstraping...', 'Bordering...', 'Borrowing...', 'Botching...', 'Bottling...',
|
|
257
|
+
'Bowing...', 'Bowling...', 'Boxing...', 'Bracing...', 'Brainstorming...', 'Braising...', 'Branding...', 'Braving...', 'Breaching...', 'Breathing...',
|
|
258
|
+
'Breezing...', 'Bricking...', 'Bridging...', 'Briefing...', 'Bringing...', 'Bristling...', 'Broadening...', 'Brokering...', 'Bronzing...', 'Brooding...',
|
|
259
|
+
'Browsing...', 'Brushing...', 'Buckling...', 'Budgeting...', 'Buffering...', 'Buffing...', 'Bulking...', 'Bulletproofing...', 'Bumping...', 'Bunching...',
|
|
260
|
+
'Buoying...', 'Bursting...', 'Burying...', 'Busting...', 'Buttering...', 'Buzzing...', 'Caching...', 'Cadencing...', 'Cajoling...', 'Caking...',
|
|
261
|
+
'Calling...', 'Calming...', 'Camping...', 'Canceling...', 'Canoodling...', 'Canvasing...', 'Capitalizing...', 'Captioning...', 'Capturing...', 'Carbonizing...',
|
|
262
|
+
'Caring...', 'Carving...', 'Cashing...', 'Casting...', 'Cataloging...', 'Catapulting...', 'Catching...', 'Categorizing...', 'Catering...', 'Cautioning...',
|
|
263
|
+
'Ceasing...', 'Celebrating...', 'Cementing...', 'Censoring...', 'Centralizing...', 'Certifying...', 'Chaining...', 'Chairing...', 'Chalking...', 'Challenging...',
|
|
264
|
+
'Channeling...', 'Chanting...', 'Charting...', 'Chating...', 'Cheering...', 'Chewing...', 'Chilling...', 'Chiming...', 'Chiping...', 'Chirping...',
|
|
265
|
+
'Chiseling...', 'Choosing...', 'Choping...', 'Choreographing...', 'Chronicling...', 'Chuging...', 'Ciphering...', 'Circling...', 'Circulating...', 'Citing...',
|
|
266
|
+
'Civilizing...', 'Clamping...', 'Clanging...', 'Claping...', 'Clarifying...', 'Clashing...', 'Classifying...', 'Clawing...', 'Cleansing...', 'Cleaving...',
|
|
267
|
+
'Climbing...', 'Clinching...', 'Cliping...', 'Cloaking...', 'Clocking...', 'Cloning...', 'Closing...', 'Clouding...', 'Clustering...', 'Clutching...',
|
|
268
|
+
'Coaching...', 'Coalescing...', 'Coating...', 'Coaxing...', 'Cocooning...', 'Codifying...', 'Coercing...', 'Coexisting...', 'Cogitating...', 'Cohering...',
|
|
269
|
+
'Coiling...', 'Coinciding...', 'Collaborating...', 'Collapsing...', 'Collating...', 'Colliding...', 'Colonizing...', 'Coloring...', 'Combing...', 'Combining...',
|
|
270
|
+
'Comforting...', 'Commemorating...', 'Commencing...', 'Commenting...', 'Commissioning...', 'Commiting...', 'Communing...', 'Communicating...', 'Commuting...', 'Compacting...',
|
|
271
|
+
'Comparing...', 'Compassing...', 'Compeling...', 'Compensating...', 'Competing...', 'Complementing...', 'Completing...', 'Complicating...', 'Complimenting...', 'Comprehending...',
|
|
272
|
+
'Compressing...', 'Comprising...', 'Compromising...', 'Concealing...', 'Conceding...', 'Conceiving...', 'Conceptualizing...', 'Concerning...', 'Concluding...', 'Concuring...',
|
|
273
|
+
'Condensing...', 'Conditioning...', 'Condoning...', 'Confering...', 'Confessing...', 'Configuring...', 'Confining...', 'Confiscating...', 'Conflating...', 'Confronting...',
|
|
274
|
+
'Confusing...', 'Congealing...', 'Congratulating...', 'Conjuring...', 'Connecting...', 'Conquering...', 'Consecrating...', 'Consenting...', 'Conserving...', 'Consigning...',
|
|
275
|
+
'Consisting...', 'Consoling...', 'Consolidating...', 'Conspiring...', 'Constituting...', 'Constraining...', 'Consulting...', 'Consuming...', 'Contacting...', 'Containing...',
|
|
276
|
+
'Contemplating...', 'Contending...', 'Contenting...', 'Contesting...', 'Contracting...', 'Contrasting...', 'Contributing...', 'Contriving...', 'Controling...', 'Convening...',
|
|
277
|
+
'Converging...', 'Conversing...', 'Converting...', 'Conveying...', 'Convincing...', 'Convoying...', 'Cooling...', 'Cooperating...', 'Coordinating...', 'Copying...',
|
|
278
|
+
'Coring...', 'Corking...', 'Correlating...', 'Corresponding...', 'Corroborating...', 'Corraling...', 'Correcting...', 'Corrugating...', 'Cosseting...', 'Counseling...',
|
|
279
|
+
'Counting...', 'Coupling...', 'Coursing...', 'Covering...', 'Coveting...', 'Cradling...', 'Cranking...', 'Crashing...', 'Crawling...', 'Creasing...',
|
|
280
|
+
'Crediting...', 'Cresting...', 'Critiquing...', 'Crocheting...', 'Crooning...', 'Crossing...', 'Crowding...', 'Crowning...', 'Crumbing...', 'Crusading...',
|
|
281
|
+
'Crystallizing...', 'Cubing...', 'Cuddling...', 'Culling...', 'Cultivating...', 'Curbing...', 'Curating...', 'Curling...', 'Currying...', 'Cushioning...',
|
|
282
|
+
'Customizing...', 'Cuting...', 'Cycling...', 'Dabbling...', 'Daming...', 'Dampening...', 'Daring...', 'Darting...', 'Dashing...', 'Dating...',
|
|
283
|
+
'Dawdling...', 'Dazzling...', 'Deactivating...', 'Debuging...', 'Debuting...', 'Decanting...', 'Decelerating...', 'Decentralizing...', 'Decking...', 'Declaring...',
|
|
284
|
+
'Declining...', 'Decomposing...', 'Decorating...', 'Decoupling...', 'Decreasing...', 'Dedicating...', 'Deducing...', 'Deepening...', 'Defeating...', 'Defending...',
|
|
285
|
+
'Defering...', 'Defining...', 'Deflecting...', 'Deforming...', 'Defragmenting...', 'Defusing...', 'Degreasing...', 'Dehydrating...', 'Delegating...', 'Deleting...',
|
|
286
|
+
'Delivering...', 'Demanding...', 'Demarcating...', 'Demisting...', 'Demystifying...', 'Denoting...', 'Denouncing...', 'Densifying...', 'Departing...', 'Depending...',
|
|
287
|
+
'Depositing...', 'Depressurizing...', 'Deputing...', 'Deriving...', 'Descending...', 'Describing...', 'Deserting...', 'Desiring...', 'Despatching...', 'Detecting...',
|
|
288
|
+
'Detering...', 'Detoxing...', 'Devaluing...', 'Developing...', 'Deviating...', 'Devoting...', 'Diping...', 'Disabling...', 'Disarming...', 'Disassembling...',
|
|
289
|
+
'Disbursing...', 'Discarding...', 'Discerning...', 'Discharging...', 'Disciplining...', 'Disclosing...', 'Disconnecting...', 'Discontinuing...', 'Discounting...', 'Discoursing...',
|
|
290
|
+
'Discrediting...', 'Discriminating...', 'Discussing...', 'Disembarking...', 'Disentangling...', 'Disguising...', 'Disinfecting...', 'Disliking...', 'Dismantling...', 'Dismissing...',
|
|
291
|
+
'Dispatching...', 'Dispeling...', 'Dispensing...', 'Dispersing...', 'Displaying...', 'Disposing...', 'Disproving...', 'Dissecting...', 'Disseminating...', 'Dissipating...',
|
|
292
|
+
'Dissolving...', 'Distinguishing...', 'Distracting...', 'Distributing...', 'Disturbing...', 'Diverting...', 'Divining...', 'Dividing...', 'Divulging...', 'Docking...',
|
|
293
|
+
'Documenting...', 'Dodging...', 'Domesticating...', 'Dominating...', 'Donating...', 'Doodling...', 'Dosing...', 'Doting...', 'Doubling...', 'Doubting...',
|
|
294
|
+
'Downgrading...', 'Downloading...', 'Draging...', 'Draining...',
|
|
281
295
|
];
|
|
282
296
|
|
|
283
297
|
// ---- ANSI helpers ----
|
|
@@ -696,15 +710,46 @@ export function composerTextIndexAt(layout, rowIdx, colInside) {
|
|
|
696
710
|
// A Bash result is a FAILURE when the command exited non-zero or the spawn
|
|
697
711
|
// itself errored. Detected from the tool-result text (the only place the code
|
|
698
712
|
// survives for the UI, since the exit-code line is hidden from display).
|
|
699
|
-
|
|
713
|
+
// Failure prefixes that the built-in tools actually return (verified against
|
|
714
|
+
// src/tools/*.js). Any result starting with one of these is a FAILED call:
|
|
715
|
+
// "Error: old_string not found in …" (Edit)
|
|
716
|
+
// "Error reading/writing <path>: …" (Read/Write/Edit)
|
|
717
|
+
// "Cannot read: <path> appears to be …" (Read)
|
|
718
|
+
// "Edit rejected: …" (Edit staleness / read guards)
|
|
719
|
+
// "Command cannot be empty." (Bash)
|
|
720
|
+
// resolvePath throws are surfaced verbatim as `e.message`, e.g.
|
|
721
|
+
// "Path outside workspace: …" / "ENOENT: no such file or directory …".
|
|
722
|
+
const FAIL_PREFIX = /^(Error\b|\[error:|Cannot read:|Edit rejected:|Command cannot be empty\b|Path outside workspace\b|ENOENT\b|EACCES\b|EPERM\b|EISDIR\b|ENOTDIR\b)/i;
|
|
723
|
+
|
|
724
|
+
export function isFailureResult(text, toolName) {
|
|
700
725
|
const s = String(text == null ? '' : text);
|
|
701
|
-
|
|
726
|
+
const trimmed = s.trim();
|
|
727
|
+
if (FAIL_PREFIX.test(trimmed)) return true;
|
|
728
|
+
// A non-zero Bash exit code marks the call as failed (red status bullet). The
|
|
729
|
+
// `[exit code: N]` LINE itself is never shown to the user — it is bookkeeping
|
|
730
|
+
// for the model only (see messageLines).
|
|
702
731
|
const m = /\[exit code:\s*([^\]]+)\]/.exec(s);
|
|
703
732
|
if (!m) return false;
|
|
704
733
|
const v = m[1].trim();
|
|
705
734
|
return v !== '0';
|
|
706
735
|
}
|
|
707
736
|
|
|
737
|
+
// Extract the human-readable failure reason from a tool result, or '' when the
|
|
738
|
+
// result should not render an extra "why it failed" line. The reason is shown
|
|
739
|
+
// in red under the tool call for non-Bash tools (Edit / Read / Write / …); Bash
|
|
740
|
+
// failures are already obvious from the command's own output, so they return ''.
|
|
741
|
+
export function failureReason(text, toolName) {
|
|
742
|
+
const name = String(toolName || '').toLowerCase();
|
|
743
|
+
if (name === 'bash') return '';
|
|
744
|
+
const s = String(text == null ? '' : text);
|
|
745
|
+
const trimmed = s.trim();
|
|
746
|
+
if (!FAIL_PREFIX.test(trimmed)) return '';
|
|
747
|
+
// Collapse to a single line and drop the `[error: …]` wrapper.
|
|
748
|
+
let line = trimmed.split('\n')[0].trim();
|
|
749
|
+
line = line.replace(/^\[error:\s*/i, '').replace(/\]$/, '').trim();
|
|
750
|
+
return line;
|
|
751
|
+
}
|
|
752
|
+
|
|
708
753
|
function msgColorFor(role, text) {
|
|
709
754
|
if (role === 'system' && typeof text === 'string' && text.startsWith('[turn took')) return C.gray;
|
|
710
755
|
if (role === 'system') return C.teal;
|
|
@@ -1228,9 +1273,10 @@ function markdownLineToRows(line, width, fg, codeFg, isContinuation) {
|
|
|
1228
1273
|
let m = /^(\s*)(#{1,6})\s+(.*)$/.exec(t);
|
|
1229
1274
|
if (m && !isContinuation) {
|
|
1230
1275
|
const hashCount = m[2].length;
|
|
1276
|
+
// h1 is bold white, h2 is plain white, h3+ is gray. Only h1 carries BOLD.
|
|
1231
1277
|
const size = hashCount === 1 ? C.white + C.bold : hashCount === 2 ? C.white : C.gray;
|
|
1232
1278
|
for (const w of wrapWords(m[3], width)) {
|
|
1233
|
-
out.push(
|
|
1279
|
+
out.push(col(w, size));
|
|
1234
1280
|
}
|
|
1235
1281
|
return out;
|
|
1236
1282
|
}
|
|
@@ -1254,7 +1300,7 @@ function markdownLineToRows(line, width, fg, codeFg, isContinuation) {
|
|
|
1254
1300
|
if (task && !isContinuation) {
|
|
1255
1301
|
const checked = task[3] !== ' ';
|
|
1256
1302
|
const box = checked ? col('✓', C.green) : col('○', C.gray);
|
|
1257
|
-
const content = col(' ' + inlineMarkdown(task[4], codeFg), fg);
|
|
1303
|
+
const content = col(' ' + inlineMarkdown(task[4], codeFg, fg), fg);
|
|
1258
1304
|
for (const w of wrapWords(task[4], Math.max(1, width - 4))) {
|
|
1259
1305
|
out.push(col(' ', C.gray) + box + (w === task[4] ? content : col(' ' + w, fg)));
|
|
1260
1306
|
}
|
|
@@ -1266,7 +1312,7 @@ function markdownLineToRows(line, width, fg, codeFg, isContinuation) {
|
|
|
1266
1312
|
const bullet = isOrdered ? col(li[2], C.cyan) : col('•', C.cyan);
|
|
1267
1313
|
const pad = isOrdered ? ' '.repeat(visualCol(li[2])) + ' ' : ' ';
|
|
1268
1314
|
let first = true;
|
|
1269
|
-
for (const w of wrapWords(inlineMarkdown(li[3], codeFg), Math.max(1, width - 2))) {
|
|
1315
|
+
for (const w of wrapWords(inlineMarkdown(li[3], codeFg, fg), Math.max(1, width - 2))) {
|
|
1270
1316
|
out.push((first ? col('', C.gray) + bullet + ' ' : col(pad, C.gray)) + col(w, fg));
|
|
1271
1317
|
first = false;
|
|
1272
1318
|
}
|
|
@@ -1275,7 +1321,7 @@ function markdownLineToRows(line, width, fg, codeFg, isContinuation) {
|
|
|
1275
1321
|
}
|
|
1276
1322
|
// Inline styles: **bold**, *italic*, `code`, [text](url)
|
|
1277
1323
|
if (/[*`\[]/.test(t)) {
|
|
1278
|
-
for (const w of wrapWords(t, width)) out.push(col(inlineMarkdown(w, codeFg), fg));
|
|
1324
|
+
for (const w of wrapWords(t, width)) out.push(col(inlineMarkdown(w, codeFg, fg), fg));
|
|
1279
1325
|
return out;
|
|
1280
1326
|
}
|
|
1281
1327
|
// Plain - apply default foreground color
|
|
@@ -1285,18 +1331,23 @@ function markdownLineToRows(line, width, fg, codeFg, isContinuation) {
|
|
|
1285
1331
|
|
|
1286
1332
|
// Apply inline markdown to a single line: `code`, **bold**, *italic*, ~~strike~~,
|
|
1287
1333
|
// [text](url). Order matters so inner tokens are handled safely.
|
|
1288
|
-
|
|
1334
|
+
// `base` is the surrounding foreground colour; every inline style re-asserts it
|
|
1335
|
+
// after its own reset, otherwise the bare `\e[0m` would cancel the base colour
|
|
1336
|
+
// for the REST of the line (plain text after a **bold** run used to lose its
|
|
1337
|
+
// colour and fall back to the terminal default).
|
|
1338
|
+
function inlineMarkdown(s, codeFg, base) {
|
|
1339
|
+
const fb = base || '';
|
|
1289
1340
|
let r = s;
|
|
1290
1341
|
// Inline code first so it is not mangled by * [] ~.
|
|
1291
|
-
r = r.replace(/`([^`]+)`/g, (_, c) => col(c, codeFg));
|
|
1342
|
+
r = r.replace(/`([^`]+)`/g, (_, c) => col(c, codeFg) + fb);
|
|
1292
1343
|
// Bold
|
|
1293
|
-
r = r.replace(/\*\*([^*]+)\*\*/g, (_, c) => C.bold + c + C.reset);
|
|
1344
|
+
r = r.replace(/\*\*([^*]+)\*\*/g, (_, c) => C.bold + c + C.reset + fb);
|
|
1294
1345
|
// Strikethrough
|
|
1295
1346
|
r = r.replace(/~~([^~]+)~~/g, (_, c) => '\x1b[9m' + c + '\x1b[29m');
|
|
1296
1347
|
// Italic
|
|
1297
1348
|
r = r.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, (_, pre, c) => pre + '\x1b[3m' + c + '\x1b[23m');
|
|
1298
1349
|
// Links: [text](url) -> text (cyan); autolink bare http(s) urls
|
|
1299
|
-
r = r.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, txt) => col(txt, C.cyan));
|
|
1350
|
+
r = r.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, txt) => col(txt, C.cyan) + fb);
|
|
1300
1351
|
r = r.replace(/(^|\s)(https?:\/\/[^\s]+)/g, (_, pre, url) => pre + col(url, C.cyan));
|
|
1301
1352
|
return r;
|
|
1302
1353
|
}
|
|
@@ -1714,6 +1765,8 @@ export function composeFrame(state, cols, rows) {
|
|
|
1714
1765
|
const hint = ed.hint || 'Ctrl+S save · Esc cancel · Enter newline';
|
|
1715
1766
|
lines.push(col(hint, C.gray));
|
|
1716
1767
|
lines.push('');
|
|
1768
|
+
// Viewport height for the editor body
|
|
1769
|
+
const viewH = Math.max(1, bodyH - (ed.notice ? 6 : 5));
|
|
1717
1770
|
// Keep the caret row in view.
|
|
1718
1771
|
let top = Math.max(0, ed.top || 0);
|
|
1719
1772
|
if (ed.caretRow < top) top = ed.caretRow;
|
|
@@ -1723,6 +1776,8 @@ export function composeFrame(state, cols, rows) {
|
|
|
1723
1776
|
const idx = top + i;
|
|
1724
1777
|
const txt = idx < textLines.length ? expandTabs(textLines[idx]) : '';
|
|
1725
1778
|
lines.push(col(fitAnsi(txt, w), C.white));
|
|
1779
|
+
// Mouse click on this body row moves the caret to that line.
|
|
1780
|
+
addHit(lines.length - 1, 0, w - 1, { kind: 'editor', row: idx });
|
|
1726
1781
|
}
|
|
1727
1782
|
lines.push(col(fitAnsi(` line ${ed.caretRow + 1}/${textLines.length} · ${textLines.length} lines`, w), C.gray));
|
|
1728
1783
|
if (ed.notice) lines.push(col(fitAnsi(' ' + ed.notice, w), ed.noticeKind === 'error' ? C.red : C.green));
|
|
@@ -1975,13 +2030,23 @@ export function composeFrame(state, cols, rows) {
|
|
|
1975
2030
|
if (state.finishAnim) {
|
|
1976
2031
|
const DUR = 500;
|
|
1977
2032
|
const t = Math.min(1, (Date.now() - state.finishAnim.start) / DUR);
|
|
1978
|
-
const
|
|
1979
|
-
const
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2033
|
+
const wordFrom = state.finishAnim.wordFrom || '';
|
|
2034
|
+
const wordTo = state.finishAnim.wordTo || 'turn took';
|
|
2035
|
+
const tail = state.finishAnim.tail || ''; // ` <duration>]`, never changes
|
|
2036
|
+
// SWEEP: start from the LIVE word (`Working...`), pad it on the right to
|
|
2037
|
+
// the wider of the two words, then overwrite it left → right, one
|
|
2038
|
+
// character per step. Characters the sweep has not reached keep their OLD
|
|
2039
|
+
// value, so each `.` disappears on its own turn:
|
|
2040
|
+
// [Working... 59s] → [turn to... 59s] → [turn too.. 59s] → [turn took 59s]
|
|
2041
|
+
// Width = the LONGER word, otherwise a longer old word gets truncated and
|
|
2042
|
+
// its trailing dots vanish at once.
|
|
2043
|
+
const width = Math.max(wordFrom.length, wordTo.length);
|
|
2044
|
+
const from = wordFrom.padEnd(width, '.');
|
|
2045
|
+
const toPadded = wordTo.padEnd(width, '');
|
|
2046
|
+
const done = t >= 1 ? width : Math.floor(t * width); // chars overwritten so far
|
|
2047
|
+
let word = '';
|
|
2048
|
+
for (let i = 0; i < width; i++) {
|
|
2049
|
+
word += (i < done) ? (toPadded[i] || '') : from[i];
|
|
1985
2050
|
}
|
|
1986
2051
|
// Colour: orange → grey over the animation.
|
|
1987
2052
|
const ORANGE = [255, 140, 0];
|
|
@@ -1990,7 +2055,7 @@ export function composeFrame(state, cols, rows) {
|
|
|
1990
2055
|
const g = Math.round(ORANGE[1] + (GREY[1] - ORANGE[1]) * t);
|
|
1991
2056
|
const b = Math.round(ORANGE[2] + (GREY[2] - ORANGE[2]) * t);
|
|
1992
2057
|
const animColor = lerpColor(r, g, b, r, g, b, 0);
|
|
1993
|
-
lines.push(col(frame, animColor) + ' ' + col(
|
|
2058
|
+
lines.push(col(frame, animColor) + ' ' + col(`[${word}${tail}`, animColor));
|
|
1994
2059
|
} else {
|
|
1995
2060
|
|
|
1996
2061
|
// Timing (spinner ticks every 80ms, so 0.5s ≈ 6 ticks):
|
|
@@ -2009,7 +2074,7 @@ export function composeFrame(state, cols, rows) {
|
|
|
2009
2074
|
// Colours as plain RGB triples (never ANSI strings).
|
|
2010
2075
|
const ORANGE = [255, 140, 0];
|
|
2011
2076
|
const YELLOW = [255, 240, 120];
|
|
2012
|
-
const RED = [255,
|
|
2077
|
+
const RED = [255, 0, 0]; // pure red
|
|
2013
2078
|
const target = phase === 0 ? YELLOW : RED;
|
|
2014
2079
|
|
|
2015
2080
|
const chars = workMsg;
|
|
@@ -3210,47 +3275,7 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h, submit, stdout, ren
|
|
|
3210
3275
|
return;
|
|
3211
3276
|
}
|
|
3212
3277
|
|
|
3213
|
-
|
|
3214
|
-
const applyTheme = (name) => {
|
|
3215
|
-
if (!setTheme(name)) return false;
|
|
3216
|
-
state.theme = name;
|
|
3217
|
-
try {
|
|
3218
|
-
const f = hncodeConfigFile();
|
|
3219
|
-
const txt = fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '';
|
|
3220
|
-
const next = /^theme\s*=.*$/m.test(txt)
|
|
3221
|
-
? txt.replace(/^theme\s*=.*$/m, `theme = "${name}"`)
|
|
3222
|
-
: `theme = "${name}"\n` + txt;
|
|
3223
|
-
fs.writeFileSync(f, next, 'utf8');
|
|
3224
|
-
} catch { }
|
|
3225
|
-
|
|
3226
|
-
// Force a complete screen refresh with new theme colors
|
|
3227
|
-
// We need to access stdout from the host context
|
|
3228
|
-
if (host && typeof host.renderFrame === 'function') {
|
|
3229
|
-
// Trigger immediate re-render which will use new theme colors
|
|
3230
|
-
host.renderFrame();
|
|
3231
|
-
} else {
|
|
3232
|
-
// Fallback: just show message and let normal flow handle it
|
|
3233
|
-
app(`Theme set to "${name}".`);
|
|
3234
|
-
return true;
|
|
3235
|
-
}
|
|
3236
|
-
|
|
3237
|
-
app(`Theme set to "${name}".`);
|
|
3238
|
-
return true;
|
|
3239
|
-
};
|
|
3240
|
-
if (raw) {
|
|
3241
|
-
if (!THEME_NAMES.includes(raw)) { appErr(`Theme "${raw}" could not be loaded. Expected: ${THEME_NAMES.join(' / ')}`); return; }
|
|
3242
|
-
applyTheme(raw); return;
|
|
3243
|
-
}
|
|
3244
|
-
openPicker({
|
|
3245
|
-
title: 'Set the terminal UI theme',
|
|
3246
|
-
items: THEME_NAMES.map((t) => ({ label: t, sub: t === 'auto' ? 'track the terminal' : `${t} palette`, current: (state.theme || 'auto') === t })),
|
|
3247
|
-
sel: Math.max(0, THEME_NAMES.indexOf(state.theme || 'auto')),
|
|
3248
|
-
searchable: false,
|
|
3249
|
-
onPick: (it) => { applyTheme(it.label); return true; },
|
|
3250
|
-
});
|
|
3251
|
-
return;
|
|
3252
|
-
}
|
|
3253
|
-
case 'statusline':
|
|
3278
|
+
case 'statusline':
|
|
3254
3279
|
openPicker({
|
|
3255
3280
|
title: 'Status line items',
|
|
3256
3281
|
items: [
|
|
@@ -4346,34 +4371,23 @@ export async function startTUI(opts) {
|
|
|
4346
4371
|
// PowerShell took ~3s, which read as "the shortcut does nothing".
|
|
4347
4372
|
async function pasteFromClipboard(fastPathOnly = false) {
|
|
4348
4373
|
notice('Pasting…', 'info');
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4374
|
+
// One probe handles every clipboard shape: copied FILES (→ paths), plain
|
|
4375
|
+
// TEXT (→ text), or an IMAGE (→ a temp .png path). The async version runs it
|
|
4376
|
+
// on the PRE-WARMED helper (~10ms) instead of a cold spawn (~2.9s).
|
|
4377
|
+
const { text, via } = await readClipboardContentAsync();
|
|
4378
|
+
if (text) {
|
|
4379
|
+
insertComposerPaste(text);
|
|
4380
|
+
notice(via === 'text' ? 'Pasted' : `Pasted ${via} as path`, 'info');
|
|
4354
4381
|
renderFrame();
|
|
4355
4382
|
return;
|
|
4356
4383
|
}
|
|
4357
|
-
|
|
4358
|
-
//
|
|
4359
|
-
try {
|
|
4360
|
-
const text = readOnce();
|
|
4361
|
-
if (text) {
|
|
4362
|
-
insertComposerPaste(text);
|
|
4363
|
-
notice('Pasted instantly!', 'info');
|
|
4364
|
-
renderFrame();
|
|
4365
|
-
return;
|
|
4366
|
-
}
|
|
4367
|
-
} catch {}
|
|
4368
|
-
|
|
4369
|
-
// Fast path failed - only use slow path for Ctrl+Shift+V, not for right-click
|
|
4384
|
+
// Fast path failed — only fall back to the slow warm-helper reader when this
|
|
4385
|
+
// is a full paste (Ctrl+Shift+V), not a right-click quick paste.
|
|
4370
4386
|
if (fastPathOnly) {
|
|
4371
|
-
notice('Clipboard empty or unavailable
|
|
4387
|
+
notice('Clipboard empty or unavailable', 'error');
|
|
4372
4388
|
renderFrame();
|
|
4373
4389
|
return;
|
|
4374
4390
|
}
|
|
4375
|
-
|
|
4376
|
-
// Slow path for Ctrl+Shift+V
|
|
4377
4391
|
const result = await readText();
|
|
4378
4392
|
if (!result.text) { notice('Clipboard empty or unavailable', 'error'); renderFrame(); return; }
|
|
4379
4393
|
insertComposerPaste(result.text);
|
|
@@ -4564,18 +4578,18 @@ export async function startTUI(opts) {
|
|
|
4564
4578
|
}
|
|
4565
4579
|
}
|
|
4566
4580
|
|
|
4567
|
-
if (t.key === 'c-s') {
|
|
4581
|
+
if (t.key === 'c-s' && !state.editor) {
|
|
4568
4582
|
steerAll();
|
|
4569
4583
|
return;
|
|
4570
4584
|
}
|
|
4571
4585
|
|
|
4572
|
-
if (t.key === 'c-t') {
|
|
4586
|
+
if (t.key === 'c-t' && !state.editor) {
|
|
4573
4587
|
state.todosExpanded = !state.todosExpanded;
|
|
4574
4588
|
renderFrame();
|
|
4575
4589
|
return;
|
|
4576
4590
|
}
|
|
4577
4591
|
|
|
4578
|
-
if (t.key === 'c-o') {
|
|
4592
|
+
if (t.key === 'c-o' && !state.editor) {
|
|
4579
4593
|
state.expanded = !state.expanded;
|
|
4580
4594
|
notice(state.expanded ? 'Expanded tool output' : 'Collapsed tool output', 'info');
|
|
4581
4595
|
renderFrame();
|
|
@@ -4588,7 +4602,7 @@ export async function startTUI(opts) {
|
|
|
4588
4602
|
// agent is running, so the idle-only branch that used to hold this was
|
|
4589
4603
|
// unreachable dead code and ↑ scrolled the chat instead.
|
|
4590
4604
|
{
|
|
4591
|
-
const overlay = state.picker || state.form || state.panel || state.menuOpen;
|
|
4605
|
+
const overlay = state.picker || state.form || state.panel || state.menuOpen || state.editor;
|
|
4592
4606
|
const cur = state.input || '';
|
|
4593
4607
|
if (!overlay && cur === '' && t.key === 'up' && (state.queued || []).length) {
|
|
4594
4608
|
recallQueued();
|
|
@@ -4623,7 +4637,7 @@ export async function startTUI(opts) {
|
|
|
4623
4637
|
}
|
|
4624
4638
|
else {
|
|
4625
4639
|
if (t.key === 'up' || t.key === 'down') {
|
|
4626
|
-
const overlay = state.picker || state.form || state.panel || state.menuOpen;
|
|
4640
|
+
const overlay = state.picker || state.form || state.panel || state.menuOpen || state.editor;
|
|
4627
4641
|
const cur = state.input || '';
|
|
4628
4642
|
if (!overlay && cur === '' && state.history.length) {
|
|
4629
4643
|
if (state.historyIdx === -1) state.historyIdx = state.history.length;
|
|
@@ -5433,9 +5447,18 @@ export async function startTUI(opts) {
|
|
|
5433
5447
|
|| [...state.chat].reverse().find((m) => m.role === 'tool' && m.pending && m.toolName === e.name);
|
|
5434
5448
|
// A failed run must be visible on the TOOL line too: the status bullet
|
|
5435
5449
|
// turns red (see messageLines).
|
|
5436
|
-
const failedRun = isFailureResult(e.content);
|
|
5450
|
+
const failedRun = isFailureResult(e.content, e.name);
|
|
5437
5451
|
if (entry) { entry.pending = false; entry.id = undefined; entry.failed = failedRun; }
|
|
5438
|
-
|
|
5452
|
+
const reason = failureReason(e.content, e.name);
|
|
5453
|
+
const isEditLike = e.name === 'Edit' || e.name === 'Write';
|
|
5454
|
+
if (!isEditLike) {
|
|
5455
|
+
// Non-Edit tools show their raw result underneath.
|
|
5456
|
+
addChat({ role: 'tool_result', text: e.content, failed: failedRun });
|
|
5457
|
+
} else if (reason) {
|
|
5458
|
+
// Edit/Write hide their body (the diff already shows the change), so
|
|
5459
|
+
// surface a failure reason as its own red line under the tool call.
|
|
5460
|
+
addChat({ role: 'tool_result', text: reason, failed: true });
|
|
5461
|
+
}
|
|
5439
5462
|
// NOTE: queued input is deliberately NOT drained into the running turn
|
|
5440
5463
|
// here. A message typed while the agent works is a NEW turn, so it waits
|
|
5441
5464
|
// for this turn to finish (see the drain after `agent.run()`). Ctrl-S
|
|
@@ -5470,16 +5493,17 @@ export async function startTUI(opts) {
|
|
|
5470
5493
|
session.focus = !!state.focus;
|
|
5471
5494
|
sess.saveSession(session);
|
|
5472
5495
|
const turnMs = state.turnStart ? Date.now() - state.turnStart : 0;
|
|
5473
|
-
const
|
|
5474
|
-
|
|
5475
|
-
//
|
|
5476
|
-
//
|
|
5477
|
-
//
|
|
5478
|
-
|
|
5496
|
+
const dur = fmtDuration(turnMs);
|
|
5497
|
+
// The live row is `<phrase> <gray>[<dur>]`. At the end we keep the same row
|
|
5498
|
+
// shape — `[<word> <dur>]` — and only rewrite the word, so the brackets and
|
|
5499
|
+
// the duration stay put and the time is never repeated. `…` is part of the
|
|
5500
|
+
// phrase and is preserved.
|
|
5501
|
+
const wordFrom = String(state.workMsg || WORKING_MESSAGES[0]);
|
|
5479
5502
|
state.finishAnim = {
|
|
5480
5503
|
start: Date.now(),
|
|
5481
|
-
|
|
5482
|
-
|
|
5504
|
+
wordFrom,
|
|
5505
|
+
wordTo: 'turn took',
|
|
5506
|
+
tail: ` ${dur}]`, // fixed right-hand side, e.g. " 59s]"
|
|
5483
5507
|
};
|
|
5484
5508
|
const animStart = Date.now();
|
|
5485
5509
|
while (Date.now() - animStart < 520) {
|
|
@@ -5493,7 +5517,8 @@ export async function startTUI(opts) {
|
|
|
5493
5517
|
state.lastTurnMs = turnMs;
|
|
5494
5518
|
session.lastTurnMs = turnMs;
|
|
5495
5519
|
sess.saveSession(session);
|
|
5496
|
-
|
|
5520
|
+
// Same shape the sweep animated into: `[turn took <dur>]`.
|
|
5521
|
+
addChat({ role: 'system', text: `[turn took ${dur}]` });
|
|
5497
5522
|
}
|
|
5498
5523
|
renderFrame();
|
|
5499
5524
|
if (state.queued.length) {
|
|
@@ -5587,7 +5612,13 @@ export async function startTUI(opts) {
|
|
|
5587
5612
|
}
|
|
5588
5613
|
|
|
5589
5614
|
function normalizeMsg(m) {
|
|
5590
|
-
|
|
5615
|
+
// Keep `failed`: a failed tool result must render in RED. Dropping it here
|
|
5616
|
+
// made every failure look like ordinary gray output.
|
|
5617
|
+
return {
|
|
5618
|
+
role: m && m.role ? m.role : 'system',
|
|
5619
|
+
text: m && m.text != null ? String(m.text) : '',
|
|
5620
|
+
failed: !!(m && m.failed),
|
|
5621
|
+
};
|
|
5591
5622
|
}
|
|
5592
5623
|
|
|
5593
5624
|
let stopped = false;
|