Skip to content

Commit 6814368

Browse files
committed
Enhance UI output handling by adding token counting feature and updating build configurations
1 parent bf266c5 commit 6814368

File tree

7 files changed

+105
-30
lines changed

7 files changed

+105
-30
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,4 @@ jobs:
7676

7777
# Ensure UI compilation path keeps working on all OSes
7878
- name: Build (ui feature)
79-
run: cargo build --features ui --verbose
79+
run: cargo build --features ui,tokens --verbose

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040

4141
# Make sure the UI build path still compiles
4242
- name: Compile check (ui feature)
43-
run: cargo build --release --features ui
43+
run: cargo build --release --features ui,tokens
4444

4545
# 2) Only if tests succeed, build & publish artifacts per-OS
4646
release:

Cargo.lock

Lines changed: 56 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ readme = "README.md"
88
repository = "https://github.com/gramistella/stitch"
99

1010
[features]
11-
default = ["ui"]
11+
default = ["ui", "tokens"]
1212
ui = ["dep:slint", "dep:rfd", "dep:arboard"]
13+
tokens = ["dep:tiktoken-rs"]
1314

1415
[dependencies]
1516
# Make the UI deps optional so they’re not pulled in for headless/test builds
1617
slint = { version = "1", optional = true }
1718
rfd = { version = "0.14", optional = true }
1819
arboard = { version = "3", optional = true }
20+
tiktoken-rs = { version = "0.7", optional = true }
1921

2022
regex = "1"
2123
anyhow = "1"
2224
chrono = { version = "0.4", default-features = false, features = ["clock"] }
23-
pathdiff = "0.2"
25+
#pathdiff = "0.2"
2426
serde_json = "1"
2527
dunce = "1"
2628
notify = "6"

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ bin-release:
1111
rustup target add "$TARGET" || true
1212
cargo build --release --features ui --target "$TARGET"
1313
else
14-
cargo build --release --features ui
14+
cargo build --release --features ui,tokens
1515
fi
1616

1717
# macOS: build .app and .dmg

src/main.rs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ use stitch::core::{
3535
#[cfg(feature = "ui")]
3636
const UI_OUTPUT_CHAR_LIMIT: usize = 50_000;
3737

38+
#[cfg(all(feature = "ui", feature = "tokens"))]
39+
use std::sync::OnceLock;
40+
41+
#[cfg(all(feature = "ui", feature = "tokens"))]
42+
use tiktoken_rs::{o200k_base, CoreBPE};
43+
3844
#[cfg(feature = "ui")]
3945
#[derive(Default)]
4046
struct AppState {
@@ -77,6 +83,7 @@ fn main() -> anyhow::Result<()> {
7783
app.set_output_text("".into());
7884
app.set_show_copy_toast(false);
7985
app.set_copy_toast_text("".into());
86+
app.set_output_stats("0 chars • 0 tokens".into());
8087

8188
let state = Rc::new(RefCell::new(AppState {
8289
poll_interval_ms: 45_000,
@@ -238,7 +245,7 @@ fn apply_selection_from_text(app: &AppWindow, state: &Rc<RefCell<AppState>>, tex
238245
for c in &node.children {
239246
walk_and_mark(c, project_root, wanted, explicit);
240247
}
241-
} else if let Some(rel) = pathdiff::diff_paths(&node.path, project_root) {
248+
} else if let Ok(rel) = node.path.strip_prefix(project_root) {
242249
let key = rel
243250
.iter()
244251
.map(|c| c.to_string_lossy())
@@ -349,18 +356,18 @@ fn on_generate_output(app: &AppWindow, state: &Rc<RefCell<AppState>>) {
349356
let mut rels = Vec::new();
350357
if want_dirs_only {
351358
for d in &dirs {
352-
if let Some(r) = pathdiff::diff_paths(d, selected_dir)
353-
&& r != PathBuf::from("")
354-
{
355-
rels.push(path_to_unix(&r));
359+
if let Ok(r) = d.strip_prefix(selected_dir) {
360+
if !r.as_os_str().is_empty() {
361+
rels.push(path_to_unix(r));
362+
}
356363
}
357364
}
358365
} else {
359366
for f in &files {
360-
if let Some(r) = pathdiff::diff_paths(f, selected_dir)
361-
&& r != PathBuf::from("")
362-
{
363-
rels.push(path_to_unix(&r));
367+
if let Ok(r) = f.strip_prefix(selected_dir) {
368+
if !r.as_os_str().is_empty() {
369+
rels.push(path_to_unix(r));
370+
}
364371
}
365372
}
366373
}
@@ -417,8 +424,10 @@ fn on_generate_output(app: &AppWindow, state: &Rc<RefCell<AppState>>) {
417424
let s_dir = { state.borrow().selected_directory.clone().unwrap() };
418425

419426
for fp in selected_files {
420-
let rel = pathdiff::diff_paths(&fp, &s_dir)
421-
.unwrap_or_else(|| PathBuf::from(fp.file_name().unwrap_or_default()));
427+
let rel: PathBuf = fp
428+
.strip_prefix(&s_dir)
429+
.map(|p| p.to_path_buf())
430+
.unwrap_or_else(|_| PathBuf::from(fp.file_name().unwrap_or_default()));
422431

423432
let mut contents = match fs::read_to_string(&fp) {
424433
Ok(c) => c,
@@ -684,14 +693,18 @@ fn set_output(app: &AppWindow, state: &Rc<RefCell<AppState>>, s: &str) {
684693
st.full_output_text = normalized.clone();
685694
}
686695

696+
// Count characters & tokens on the FULL output (not the truncated view)
687697
let total_chars = normalized.chars().count();
698+
let total_tokens = count_tokens(&normalized);
699+
app.set_output_stats(format!("{} chars • {} tokens", total_chars, total_tokens).into());
688700

689701
// Build the displayed string (≤ limit) and add a concise footer if truncated
690702
let displayed: String = if total_chars <= UI_OUTPUT_CHAR_LIMIT {
691703
normalized.clone()
692704
} else {
705+
// Using a couple more /n than necessary because I couldn't get padding to work in the UI
693706
let footer = format!(
694-
"\n… [truncated: showing {} of {} chars — use “Copy Output” to copy all]\n",
707+
"\n… [truncated: showing {} of {} chars — use “Copy Output” to copy all]\n\n\n",
695708
UI_OUTPUT_CHAR_LIMIT, total_chars
696709
);
697710
// Ensure we stay within the hard UI limit, including the footer itself
@@ -832,3 +845,16 @@ fn start_fs_watcher(app: &AppWindow, state: &Rc<RefCell<AppState>>) -> notify::R
832845

833846
Ok(())
834847
}
848+
849+
#[cfg(all(feature = "ui", feature = "tokens"))]
850+
fn count_tokens(text: &str) -> usize {
851+
// Build once, reuse forever
852+
static BPE: OnceLock<CoreBPE> = OnceLock::new();
853+
let bpe = BPE.get_or_init(|| o200k_base().expect("failed to load o200k_base BPE"));
854+
bpe.encode_with_special_tokens(text).len()
855+
}
856+
857+
#[cfg(all(feature = "ui", not(feature = "tokens")))]
858+
fn count_tokens(text: &str) -> usize {
859+
text.split_whitespace().filter(|s| !s.is_empty()).count()
860+
}

ui/app.slint

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export component AppWindow inherits Window {
7878
in-out property <string> last-refresh;
7979
in-out property <string> output-text;
8080
in-out property <[string]> output-lines;
81-
81+
in-out property <string> output-stats;
8282

8383

8484
in-out property <bool> show-copy-toast;
@@ -235,9 +235,10 @@ export component AppWindow inherits Window {
235235

236236
// RIGHT PANE — output
237237
VerticalBox {
238+
padding-top: 24px;
238239
spacing: 6px;
239240
horizontal-stretch: 2;
240-
Text { text: "Output:"; }
241+
Text { text: "Output: " + root.output-stats; }
241242

242243
Rectangle {
243244
border-width: 1px;
@@ -247,7 +248,7 @@ export component AppWindow inherits Window {
247248
vertical-stretch: 1;
248249
border-color: Palette.border;
249250
background: Palette.alternate-background.darker(0.06);
250-
251+
251252
TextEdit {
252253
text <=> root.output-text;
253254

0 commit comments

Comments
 (0)