Skip to content

Commit e60bd6e

Browse files
committed
Post the notch notice without a model label, and follow the notch app's rename
Developer: Seif Hashish Project: HashCortX Summary: The finished notice carried the model that answered as a subtitle. It is read at a glance, after the work is already over, so it offers nothing to act on while crowding out the words being looked for — and it put a detail about the user's work into a shared file any process on the machine can read, for no benefit. The record is now the title and nothing else, and the subtitle argument is gone from the API and both call sites rather than left as a way to put it back by accident. The notch app is now called HashNotch and its feed folder moved with it. A poster has to land where the installed copy is looking, and this app cannot know which copy that is, so the folder is chosen at write time: `~/.hashnotch` when it exists, `~/.hashdisland` when only that does, and the current name created when neither is there. The logo path follows the same choice, so a mark is never looked for in a folder the feed is not in. The on-disk contract in the local rules moves with it: the record shape is unchanged, only its location and one optional field that is no longer sent. README, ARCHITECTURE and the ecosystem card in Settings now name HashNotch and the new path, and the README no longer says a model name is sent. The ARCHITECTURE line count for notch.rs is re-measured. Verification: npm run check — all suites pass. cargo check — clean. cargo test — 93 pass, including four new ones covering the folder choice on a fresh machine, on one still holding the old folder, on one holding both, and the record carrying no subtitle field. npm run tauri dev — built and launched with these changes; the frontend loads (a parse error in platform/index.js would take every later script with it) and the command is registered. A real model run was not triggered.
1 parent b706660 commit e60bd6e

9 files changed

Lines changed: 117 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,20 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
88

99
## [Unreleased]
1010

11-
Nothing yet. See the open items at the end of 2.5.0 for what is known and not
12-
finished.
11+
### Changed
12+
13+
- **The notch notice says "HashCortX finished" and nothing else.** It used to
14+
carry the model that answered underneath it. That line is read at a glance,
15+
after the work is already over, so it offers nothing to act on while crowding
16+
the words being looked for — and it put a detail about the user's work into a
17+
file any process on the machine can read, for no benefit.
18+
- **The notch app is called HashNotch, and its feed folder moved with it.** The
19+
notice is written to whichever folder is already on the machine, preferring
20+
`~/.hashnotch` and falling back to `~/.hashdisland`, so it lands where the
21+
installed copy is looking whichever version that is. Only when neither exists
22+
is the current one created.
23+
24+
See the open items at the end of 2.5.0 for what is known and not finished.
1325

1426
---
1527

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,12 @@ Four local-first apps, same principles — no cloud, no telemetry, your data sta
249249
| **HashCortx** *(you are here)* | The local-first AI workspace | MIT |
250250
| [**HashCerebrum**](https://github.com/Hash-7777/HashCerebrum) | Medical research workbench with a 3D brain interface | AGPL-3.0 |
251251
| [**HashMeterAi**](https://github.com/Hash-7777/HashMeterAi) | An honest local meter for how much AI you actually use | Apache-2.0 |
252-
| [**Hash D Island**](https://github.com/Hash-7777/Hash-D-Island) | Turns the MacBook notch into a live activity island | GPL-3.0 |
252+
| [**HashNotch**](https://github.com/Hash-7777/HashNotch) | Turns the MacBook notch into a live activity island | GPL-3.0 |
253253

254254
They interlock, through files on your disk rather than a service:
255255

256256
- HashCortx appends real token counts to `~/.hashcortx/usage.jsonl`, and **HashMeterAi** reads it — so your spend across every tool is measured in one place, by software that never phones home.
257-
- When a run finishes, HashCortx posts a short notice to `~/.hashdisland/activities.json`, and **Hash D Island** lights up the notch — a title and a model name at most, never a prompt or an answer. If it is not installed, the file simply sits there unread.
257+
- When a run finishes, HashCortx posts a short notice to `~/.hashnotch/activities.json`, and **HashNotch** lights up the notch — a title and nothing else, never a model name, a prompt or an answer. If it is not installed, the file simply sits there unread.
258258

259259
---
260260

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ HashCortX/
9898
│ │ │ ├── keychain.rs 103 one-time migration out of the old Keychain
9999
│ │ │ ├── export.rs 261 writes a file the user named in a save dialog
100100
│ │ │ ├── usage_log.rs 93 appends token counts to usage.jsonl
101-
│ │ │ ├── notch.rs 92 Hash D Island live-activity ping
101+
│ │ │ ├── notch.rs 160 HashNotch live-activity ping
102102
│ │ │ └── audit.rs 52 append-only audit log
103103
│ │ └── security/
104104
│ │ └── denylist.rs 648 hardcoded blocked paths and commands

src-tauri/src/commands/notch.rs

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,63 @@
11
// ==============================================================
2-
// Hash D Island live-activity ping
2+
// HashNotch live-activity ping
33
//
44
// When a run finishes, HashCortX writes one short "finished" activity to
5-
// Hash D Island's local feed so the notch lights up like the iPhone Dynamic
6-
// Island — the same way Claude Code's hook does. Metadata only: a title and
7-
// an optional model label, never any prompt or answer content.
5+
// HashNotch's local feed so the notch lights up like the iPhone Dynamic
6+
// Island — the same way Claude Code's hook does. Metadata only: a title,
7+
// never a model label and never any prompt or answer content.
88
//
9-
// The feed is Hash D Island's documented merge-by-id contract:
10-
// ~/.hashdisland/activities.json — an array of
9+
// The feed is HashNotch's documented merge-by-id contract:
10+
// ~/.hashnotch/activities.json — an array of
1111
// { id, icon, title, subtitle?, endsAt? (ISO8601), dismissAfter?, image? }
1212
//
1313
// We replace our own previous activity, leave every other poster's alone,
1414
// keep the file bounded, and write atomically so a reader never sees a
15-
// half-written file. Best-effort: if Hash D Island isn't installed the file just
15+
// half-written file. Best-effort: if HashNotch isn't installed the file just
1616
// sits there unread, and any failure is swallowed by the caller.
1717
// ==============================================================
1818

1919
use serde_json::Value;
2020
use std::fs;
2121
use std::path::PathBuf;
2222

23-
/// Hash D Island itself only shows a handful; keep the file from ever growing.
23+
/// HashNotch itself only shows a handful; keep the file from ever growing.
2424
const MAX_ACTIVITIES: usize = 8;
2525

26+
/// The folder HashNotch reads, newest name first.
27+
///
28+
/// The app was renamed and its folder moved with it. A poster has to land where
29+
/// the INSTALLED copy is looking, and this app cannot know which copy that is —
30+
/// so it writes to whichever folder is already on the machine, preferring the
31+
/// current name. Only when neither exists does it create the current one, which
32+
/// is the right guess for anyone installing from here on.
33+
const FEED_DIRS: [&str; 2] = [".hashnotch", ".hashdisland"];
34+
35+
fn feed_dir() -> PathBuf {
36+
feed_dir_in(&dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")))
37+
}
38+
39+
/// Split out from `feed_dir` so the choice can be tested against a real
40+
/// directory rather than only against whatever this machine happens to have.
41+
fn feed_dir_in(home: &PathBuf) -> PathBuf {
42+
FEED_DIRS
43+
.iter()
44+
.map(|name| home.join(name))
45+
.find(|dir| dir.is_dir())
46+
.unwrap_or_else(|| home.join(FEED_DIRS[0]))
47+
}
48+
2649
fn feed_path() -> PathBuf {
27-
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
28-
home.join(".hashdisland").join("activities.json")
50+
feed_dir().join("activities.json")
2951
}
3052

3153
/// Where this app's logo would live, if the user has put one there.
3254
///
3355
/// The notch shows a tool's own mark instead of a generic symbol when it can
3456
/// find one. Nothing is shipped or created here: the path is simply offered,
35-
/// and Hash D Island ignores it unless it names a readable image, in which case
57+
/// and HashNotch ignores it unless it names a readable image, in which case
3658
/// the symbol is drawn as before.
3759
fn logo_path(id: &str) -> PathBuf {
38-
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
39-
home.join(".hashdisland")
40-
.join("logos")
41-
.join(format!("{id}.png"))
60+
feed_dir().join("logos").join(format!("{id}.png"))
4261
}
4362

4463
#[tauri::command]
@@ -90,3 +109,52 @@ pub fn notch_activity_post(record: Value) -> Result<(), String> {
90109
fs::rename(&tmp, &path).map_err(|e| e.to_string())?;
91110
Ok(())
92111
}
112+
113+
#[cfg(test)]
114+
mod tests {
115+
use super::*;
116+
117+
fn scratch(name: &str) -> PathBuf {
118+
let dir = std::env::temp_dir().join(format!("hashcortx-notch-{name}-{}", std::process::id()));
119+
let _ = fs::remove_dir_all(&dir);
120+
fs::create_dir_all(&dir).unwrap();
121+
dir
122+
}
123+
124+
#[test]
125+
fn the_current_folder_is_used_when_nothing_exists_yet() {
126+
let home = scratch("fresh");
127+
assert_eq!(feed_dir_in(&home), home.join(".hashnotch"));
128+
let _ = fs::remove_dir_all(&home);
129+
}
130+
131+
#[test]
132+
fn a_machine_still_on_the_old_folder_is_written_to_there() {
133+
let home = scratch("old");
134+
fs::create_dir_all(home.join(".hashdisland")).unwrap();
135+
assert_eq!(feed_dir_in(&home), home.join(".hashdisland"));
136+
let _ = fs::remove_dir_all(&home);
137+
}
138+
139+
#[test]
140+
fn the_current_folder_wins_when_both_are_present() {
141+
let home = scratch("both");
142+
fs::create_dir_all(home.join(".hashdisland")).unwrap();
143+
fs::create_dir_all(home.join(".hashnotch")).unwrap();
144+
assert_eq!(feed_dir_in(&home), home.join(".hashnotch"));
145+
let _ = fs::remove_dir_all(&home);
146+
}
147+
148+
#[test]
149+
fn the_notice_carries_no_model_and_no_content() {
150+
// The record the frontend now sends, verbatim.
151+
let record: Value = serde_json::from_str(
152+
r#"{"id":"hashcortx","icon":"checkmark.circle.fill","title":"HashCortX finished","dismissAfter":3}"#,
153+
)
154+
.unwrap();
155+
let line = serde_json::to_string(&record).unwrap();
156+
assert!(line.contains("HashCortX finished"));
157+
// The one field the model label used to travel in.
158+
assert!(!line.contains("subtitle"));
159+
}
160+
}

src-tauri/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pub fn run() {
7777
embed_available,
7878
// HashMeter ecosystem — token-usage log
7979
usage_log_append,
80-
// Hash D Island — "finished" live-activity ping
80+
// HashNotch — "finished" live-activity ping
8181
notch_activity_post,
8282
// Reading a web page: resolved, judged and fetched here, so the
8383
// connection goes to the address that was checked.

src/core/settings/panel.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,8 @@ <h2 id="settingsTitle">Settings</h2>
282282
against the source it cites, and writes up what it actually found.</span>
283283
</a>
284284

285-
<a class="about-eco-app" data-eco-url="https://github.com/Hash-7777/Hash-D-Island">
286-
<span class="about-eco-name">Hash D Island <span class="about-eco-link">works with this app</span></span>
285+
<a class="about-eco-app" data-eco-url="https://github.com/Hash-7777/HashNotch">
286+
<span class="about-eco-name">HashNotch <span class="about-eco-link">works with this app</span></span>
287287
<span class="about-eco-desc">Turns the notch on your Mac into a live status bar. HashCortX lights it up
288288
the moment a long run finishes, so you can look away and still know.</span>
289289
</a>

src/js/app.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5055,10 +5055,10 @@ Tools: remember_fact / recall_facts — save the user's target roles, industries
50555055
}
50565056
state.streaming = false;
50575057
sendBtn.textContent = "Send";
5058-
// Light up Hash D Island — "HashCortX finished", like the iPhone island.
5058+
// Light up HashNotch — "HashCortX finished", like the iPhone island.
50595059
// Once per chat turn, cloud or local. Coder mode has its own loop and
50605060
// fires its own, at the end of startRun in modes/code/mode.js.
5061-
HC.notch?.finished((modelEl.value || "").split(":").pop());
5061+
HC.notch?.finished();
50625062
if (tpsBtn && !assistant.tps) {
50635063
tpsBtn.className = "ping-btn tps-btn";
50645064
if (tpsVal) tpsVal.textContent = "— t/s";

src/modes/code/mode.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2360,11 +2360,11 @@ ${conversationMsgs.filter(m => m.role !== 'system').map(m => `
23602360
if (stopBtn) stopBtn.style.display = 'none';
23612361
runAbort = null;
23622362
setRouterChip('Auto', '');
2363-
// Light up Hash D Island. app.js fires this for a chat turn, but Coder
2363+
// Light up HashNotch. app.js fires this for a chat turn, but Coder
23642364
// has its own loop and never reached that line — so the one kind of run
23652365
// long enough that you would switch away from it was the one that never
2366-
// told you it had finished. Metadata only: a model label at most.
2367-
try { window.HC?.notch?.finished((coderModel || '').split(':').pop() || 'Coder'); } catch {}
2366+
// told you it had finished. The title and nothing else.
2367+
try { window.HC?.notch?.finished(); } catch {}
23682368
}
23692369
}
23702370

src/platform/index.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ HC.usageLog = {
2626
: Promise.resolve(),
2727
};
2828

29-
// Hash D Island ping — light up the notch "HashCortX finished" when a run
29+
// HashNotch ping — light up the notch "HashCortX finished" when a run
3030
// completes, like the iPhone Dynamic Island (the same feed Claude Code's
31-
// hook writes). Best-effort and metadata-only — a model label at most,
32-
// never message content. No-op in the browser or if Hash D Island isn't there.
31+
// hook writes). Best-effort and metadata-only — the title and nothing else,
32+
// never message content. No-op in the browser or if HashNotch isn't there.
3333
HC.notch = {
34-
finished: (subtitle) => {
34+
finished: () => {
3535
if (!HC.isTauri) return Promise.resolve();
3636
// A notice, not a countdown: this has already happened, so the notch shows
3737
// it briefly and drops it rather than ticking a timer down beside the word
@@ -47,8 +47,11 @@ HC.notch = {
4747
.toISOString()
4848
.replace(/\.\d+Z$/, "Z"),
4949
};
50-
const sub = (subtitle || "").toString().trim();
51-
if (sub) record.subtitle = sub.slice(0, 120);
50+
// No subtitle. The notice used to carry the model that answered, which is
51+
// read at a glance and after the work is already over: nothing to act on,
52+
// and it crowds out the one word being looked for. It is also a detail
53+
// about the user's work leaving this app for a file any process can read,
54+
// for no benefit. The title is the whole message.
5255
return HC.invoke("notch_activity_post", { record }).catch(() => {});
5356
},
5457
};

0 commit comments

Comments
 (0)