heartcode 0.1.24 → 0.1.25
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/CHANGELOG.md +12 -0
- package/bin/heartcode.js +47 -24
- package/package.json +1 -1
- package/src/ui.js +4 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
What changed in each version of HeartCode, newest first.
|
|
4
4
|
|
|
5
|
+
## 0.1.25 (2026-10-31)
|
|
6
|
+
|
|
7
|
+
- The model(s) you choose for a folder with /select_model are remembered per folder, in
|
|
8
|
+
.heartcode/models.json, and restored when you open that folder again; auto stays auto (the file
|
|
9
|
+
is removed). A -m model on the command line still wins for that run.
|
|
10
|
+
- The status bar and the working spinner use two lines instead of cramming everything onto one: the
|
|
11
|
+
status shows the model and what it can do on one line, the run's stats and controls on the next;
|
|
12
|
+
the spinner shows what it's doing, with the elapsed time and Esc on the line below.
|
|
13
|
+
- When a chosen set or auto rotates to a fallback (the model answering now isn't your first/preferred
|
|
14
|
+
one, because it failed), the status bar marks that model with a yellow "↻" so you can see at a
|
|
15
|
+
glance it came after rotating. No rotation, or back on the first model: no marker.
|
|
16
|
+
|
|
5
17
|
## 0.1.24 (2026-10-31)
|
|
6
18
|
|
|
7
19
|
- A pinned model (one picked with /select_model, or -m) is now used by the helpers it spawns too:
|
package/bin/heartcode.js
CHANGED
|
@@ -168,6 +168,23 @@ class App {
|
|
|
168
168
|
this.director = new Director(this) // the manage tool: skills, MCP servers and settings by asking
|
|
169
169
|
this.later = [] // setting changes that wait until the current message is done
|
|
170
170
|
this.failures = {} // "provider model" → failed requests this session (engine: onFail)
|
|
171
|
+
this.loadProjectModels() // the model(s) you last chose for this folder (.heartcode/models.json)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Per folder: the model set you picked with /select_model, remembered in .heartcode/models.json and
|
|
175
|
+
// restored when you open this folder again. Absent/empty means auto (your whole list). A -m model
|
|
176
|
+
// from the command line (cfg.session already set) wins and isn't overwritten.
|
|
177
|
+
loadProjectModels() {
|
|
178
|
+
if (this.cfg.session) return
|
|
179
|
+
const saved = readJson(path.join(this.root, '.heartcode', 'models.json'), null)
|
|
180
|
+
const valid = (saved?.session ?? []).filter((t) => t?.provider && t?.model && this.cfg.providers[t.provider])
|
|
181
|
+
if (valid.length) this.cfg.session = valid
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
saveProjectModels() {
|
|
185
|
+
const file = path.join(this.root, '.heartcode', 'models.json')
|
|
186
|
+
if (this.cfg.session?.length) writeJson(file, { session: this.cfg.session.map((t) => ({ provider: t.provider, model: t.model })) })
|
|
187
|
+
else fs.rmSync(file, { force: true }) // auto: forget the folder's choice
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
newMcp() {
|
|
@@ -593,39 +610,42 @@ class App {
|
|
|
593
610
|
// one model (pinned): just its name. Several chosen for this session: the one in use + "N chosen".
|
|
594
611
|
// Your whole list (auto): "auto (in use) (N)". ↻ shows the rotation position while rotating.
|
|
595
612
|
const count = this.sessionModels().length
|
|
596
|
-
|
|
613
|
+
// the model answering now; a "↻" before it, in yellow, when it's a fallback (rotated to after one
|
|
614
|
+
// failed) — so you can see at a glance it isn't the first/preferred one
|
|
615
|
+
const rotated = rot && rot.at > 0
|
|
616
|
+
const name = rotated ? `${c.yellow('↻ ' + now.model)}` : c.info(now.model)
|
|
597
617
|
const model =
|
|
598
618
|
count === 1
|
|
599
|
-
?
|
|
619
|
+
? name
|
|
600
620
|
: cfg.session
|
|
601
|
-
? `${
|
|
602
|
-
: `${c.muted('auto')} ${c.faint('(')}${
|
|
603
|
-
|
|
621
|
+
? `${name} ${c.faint(`(${count} chosen)`)}`
|
|
622
|
+
: `${c.muted('auto')} ${c.faint('(')}${name}${c.faint(')')} ${c.faint(`(${count})`)}`
|
|
623
|
+
// two lines so nothing is crammed: the model and what it can do, then the run's stats and controls
|
|
624
|
+
const first = [`${c.brand('●')} ${c.muted(now.provider)} ${c.faint(`(${on})`)} ${c.ghost('·')} ${model}`]
|
|
625
|
+
const second = []
|
|
604
626
|
const key = `${now.provider}/${now.model}`
|
|
605
627
|
if (this.profileKey !== key) [this.profileKey, this.profile] = [key, this.s?.profile ?? loadProfile(this.client(now.provider, now.model))]
|
|
606
628
|
const profile = this.s?.profile ?? this.profile
|
|
607
629
|
if (profile) {
|
|
608
630
|
const strat = this.s?.strat ?? strategy(profile, cfg)
|
|
609
|
-
|
|
610
|
-
if (strat.thinking)
|
|
611
|
-
if (strat.vision)
|
|
631
|
+
first.push(c.muted(strat.actions === 'tools' ? 'tools' : 'guided'))
|
|
632
|
+
if (strat.thinking) first.push(c.think(`thinking${cfg.effort ? ` · ${cfg.effort}` : ''}`) + (ui.showThinking ? '' : c.faint(' (collapsed)')))
|
|
633
|
+
if (strat.vision) first.push(c.muted('images'))
|
|
612
634
|
const used = this.s?.client.lastTokens ?? 0
|
|
613
635
|
const pct = Math.round((used / strat.context) * 100)
|
|
614
636
|
const paint = pct >= 80 ? c.red : pct >= 50 ? c.yellow : c.green
|
|
615
637
|
const guess = strat.contextSure ? '' : '~' // not said by the server: assumed, and growing as bigger requests work
|
|
616
|
-
|
|
638
|
+
second.push(`${c.faint('ctx')} ${paint(`${kTokens(used)}/${guess}${kTokens(strat.context)}`)} ${c.faint(`${pct}%`)}`)
|
|
617
639
|
const tps = this.s?.client.tps
|
|
618
|
-
if (tps)
|
|
619
|
-
if (this.chat.usage?.priced)
|
|
640
|
+
if (tps) second.push(`${c.muted(String(tps))} ${c.faint('tok/s')}`)
|
|
641
|
+
if (this.chat.usage?.priced) second.push(c.muted(money(this.chat.usage.cost)))
|
|
620
642
|
} else {
|
|
621
|
-
|
|
643
|
+
first.push(c.dim('not probed yet'))
|
|
622
644
|
}
|
|
623
|
-
if (cfg.autoCompact === false)
|
|
624
|
-
if (this.worktree)
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
parts.push(perm) // Shift+Tab changes it (see the header)
|
|
628
|
-
return parts.join(sep)
|
|
645
|
+
if (cfg.autoCompact === false) second.push(c.faint('auto-compact off'))
|
|
646
|
+
if (this.worktree) second.push(c.info(`⎇ ${this.worktree.branch}`))
|
|
647
|
+
second.push({ all: c.green('⏵⏵ all allowed'), commands: c.yellow('ask before commands'), ask: c.yellow('ask before changes'), plan: c.info('⏸ plan mode') }[ui.permission]) // Shift+Tab changes it
|
|
648
|
+
return [first.join(sep), second.join(sep)].filter((l) => l.replace(/\x1b\[[0-9;]*m/g, '').trim()).join('\n')
|
|
629
649
|
}
|
|
630
650
|
|
|
631
651
|
client(name, model) {
|
|
@@ -771,20 +791,23 @@ class App {
|
|
|
771
791
|
return cfg.session.filter((t) => cfg.providers[t.provider] && (all || !cfg.providers[t.provider].off) && t.model && !seen.has(`${t.provider}/${t.model}`) && seen.add(`${t.provider}/${t.model}`))
|
|
772
792
|
}
|
|
773
793
|
|
|
774
|
-
// Use a chosen set of models for this session
|
|
775
|
-
//
|
|
794
|
+
// Use a chosen set of models for this session: one pins it (no rotation), several rotate among
|
|
795
|
+
// just those. Remembered for this folder (.heartcode/models.json); the saved ranking is untouched.
|
|
776
796
|
async setSession(list) {
|
|
777
797
|
const { cfg } = this
|
|
778
|
-
cfg.session = list //
|
|
798
|
+
cfg.session = list // the global #1 (cfg.provider/model) stays, so your whole list and the picker are unchanged
|
|
799
|
+
this.saveProjectModels()
|
|
779
800
|
this.s = null
|
|
780
|
-
this.ui.print(list.length === 1 ? `${c.green('using')} ${c.cyan(list[0].model)} ${c.dim('· pinned for this
|
|
801
|
+
this.ui.print(list.length === 1 ? `${c.green('using')} ${c.cyan(list[0].model)} ${c.dim('· pinned for this folder')}` : `${c.green(`using ${list.length} models for this folder`)}: ${c.cyan(list.map((t) => t.model).join(', '))}`)
|
|
781
802
|
await this.session()
|
|
782
803
|
}
|
|
783
804
|
|
|
784
|
-
// Back to auto: your whole list again (#1, then the next when one fails), following the saved
|
|
805
|
+
// Back to auto: your whole list again (#1, then the next when one fails), following the saved
|
|
806
|
+
// ranking. The folder's remembered choice is cleared.
|
|
785
807
|
async auto() {
|
|
786
808
|
const { cfg } = this
|
|
787
|
-
delete cfg.session
|
|
809
|
+
delete cfg.session
|
|
810
|
+
this.saveProjectModels()
|
|
788
811
|
this.s = null
|
|
789
812
|
this.ui.print(`${c.green('using')} ${c.dim('auto')} ${c.cyan(`(${this.myModels().length} models)`)}`)
|
|
790
813
|
await this.session()
|
package/package.json
CHANGED
package/src/ui.js
CHANGED
|
@@ -885,15 +885,14 @@ export class UI {
|
|
|
885
885
|
const hiddenBelow = input.rows.length - first - box.length
|
|
886
886
|
const rule = c.ghost('─'.repeat(cols))
|
|
887
887
|
const note = (n, arrow) => c.ghost('── ') + c.faint(`${arrow} ${n} more line${n > 1 ? 's' : ''}`) + c.ghost(` ${'─'.repeat(Math.max(0, cols - 18 - String(n).length))}`)
|
|
888
|
-
const below = [hiddenBelow ? note(hiddenBelow, '↓') : rule, ...(this.status ?
|
|
888
|
+
const below = [hiddenBelow ? note(hiddenBelow, '↓') : rule, ...(this.status ? this.status().split('\n').map((l) => fit(' ' + l, cols)) : [])] // the status bar, one line or more
|
|
889
889
|
const bottom = []
|
|
890
890
|
if (this.timer && !this.asking) {
|
|
891
|
-
//
|
|
891
|
+
// the spinner on its own line; the timer (and Esc) on the next, so neither line is crammed
|
|
892
892
|
const request = this.requestSince?.()
|
|
893
893
|
const timer = this.busySince ? `responding ${duration(this.worked())}${request ? ` · waiting for model ${duration(Date.now() - request)}` : ''} · Esc to stop` : ''
|
|
894
|
-
|
|
895
|
-
bottom.push(fit(
|
|
896
|
-
if (retrying && timer) bottom.push(fit(` ${c.faint(timer)}`, cols))
|
|
894
|
+
bottom.push(fit(`${c.brand(FRAMES[this.frame % FRAMES.length])} ${c.muted(this.#label())}`, cols))
|
|
895
|
+
if (timer) bottom.push(fit(` ${c.faint(timer)}`, cols))
|
|
897
896
|
}
|
|
898
897
|
if (this.scroll) bottom.push(c.dim(` ↓ ${this.scroll} more lines below · scroll down, Shift+↓ or PgDn`))
|
|
899
898
|
if (this.ahead.length) {
|